From e5420d5c8ecec6beead8573c246115c94be78eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 10 Dec 2024 18:29:57 -0300 Subject: [PATCH 001/104] Setup client to send requests --- Cargo.lock | 221 +++++++++++- crates/signer/Cargo.toml | 11 + crates/signer/build.rs | 6 + crates/signer/proto/dirk/accountmanager.proto | 65 ++++ crates/signer/proto/dirk/dkg.proto | 70 ++++ crates/signer/proto/dirk/endpoint.proto | 16 + crates/signer/proto/dirk/eth2.proto | 34 ++ crates/signer/proto/dirk/lister.proto | 47 +++ crates/signer/proto/dirk/responsestate.proto | 21 ++ crates/signer/proto/dirk/signer.proto | 86 +++++ crates/signer/proto/dirk/walletmanager.proto | 43 +++ .../third-party/google/api/annotations.proto | 31 ++ .../proto/third-party/google/api/http.proto | 318 ++++++++++++++++++ crates/signer/src/dirk.rs | 68 ++++ crates/signer/src/lib.rs | 1 + crates/signer/src/service.rs | 42 ++- 16 files changed, 1058 insertions(+), 22 deletions(-) create mode 100644 crates/signer/build.rs create mode 100644 crates/signer/proto/dirk/accountmanager.proto create mode 100644 crates/signer/proto/dirk/dkg.proto create mode 100644 crates/signer/proto/dirk/endpoint.proto create mode 100644 crates/signer/proto/dirk/eth2.proto create mode 100644 crates/signer/proto/dirk/lister.proto create mode 100644 crates/signer/proto/dirk/responsestate.proto create mode 100644 crates/signer/proto/dirk/signer.proto create mode 100644 crates/signer/proto/dirk/walletmanager.proto create mode 100644 crates/signer/proto/third-party/google/api/annotations.proto create mode 100644 crates/signer/proto/third-party/google/api/http.proto create mode 100644 crates/signer/src/dirk.rs diff --git a/Cargo.lock b/Cargo.lock index 6f512c5b..b2ac40b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + [[package]] name = "arbitrary" version = "1.3.2" @@ -1082,9 +1088,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde", ] @@ -1233,8 +1239,11 @@ dependencies = [ "k256", "lazy_static", "prometheus", + "prost", "thiserror", "tokio", + "tonic", + "tonic-build", "tracing", "tree_hash 0.8.0", "tree_hash_derive", @@ -1516,9 +1525,9 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ "generic-array", "subtle", @@ -2032,6 +2041,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fnv" version = "1.0.7" @@ -2398,9 +2413,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.3.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -2417,6 +2432,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -2435,9 +2463,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", @@ -2448,7 +2476,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower 0.4.13", "tower-service", "tracing", ] @@ -2744,6 +2771,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + [[package]] name = "native-tls" version = "0.2.11" @@ -3034,6 +3067,16 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.6.0", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -3094,6 +3137,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn 2.0.77", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -3182,6 +3235,58 @@ dependencies = [ "unarray", ] +[[package]] +name = "prost" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" +dependencies = [ + "heck 0.5.0", + "itertools 0.13.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.77", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "prost-types" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +dependencies = [ + "prost", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -3475,6 +3580,21 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +dependencies = [ + "log", + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -3487,9 +3607,20 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] [[package]] name = "rustversion" @@ -3939,9 +4070,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.4.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -4162,11 +4293,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -4232,6 +4373,52 @@ dependencies = [ "winnow 0.6.13", ] +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "rustls-pemfile", + "socket2", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.77", +] + [[package]] name = "tower" version = "0.4.13" @@ -4240,9 +4427,13 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "indexmap 1.9.3", "pin-project", "pin-project-lite", + "rand", + "slab", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 9eba3fd7..2a3bffb9 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -37,3 +37,14 @@ uuid.workspace = true bimap.workspace = true lazy_static.workspace = true derive_more.workspace = true + +tonic = { version = "0.12.3", features = [ + "tls", + "channel", + "prost", + "codegen", +] } +prost = "0.13.4" + +[build-dependencies] +tonic-build = "0.12.3" diff --git a/crates/signer/build.rs b/crates/signer/build.rs new file mode 100644 index 00000000..3407eacf --- /dev/null +++ b/crates/signer/build.rs @@ -0,0 +1,6 @@ +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_server(false) + .compile_protos(&["proto/dirk/lister.proto"], &["proto/dirk", "proto/third-party"])?; + Ok(()) +} diff --git a/crates/signer/proto/dirk/accountmanager.proto b/crates/signer/proto/dirk/accountmanager.proto new file mode 100644 index 00000000..6ca3abb8 --- /dev/null +++ b/crates/signer/proto/dirk/accountmanager.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package v1; + +import "google/api/annotations.proto"; +import "responsestate.proto"; +import "endpoint.proto"; + +option csharp_namespace = "Eth2Signer.v1"; +option php_namespace = "Eth2Signer\\v1"; +option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; +option java_package = "com.wealdtech.eth2signerapi.v1"; +option java_multiple_files = true; +option java_outer_classname = "AccountManagerProto"; + +service AccountManager { + rpc Unlock(UnlockAccountRequest) returns (UnlockAccountResponse) { + option (google.api.http) = { + get: "/v1/accountmanager/unlock" + }; + } + + rpc Lock(LockAccountRequest) returns (LockAccountResponse) { + option (google.api.http) = { + get: "/v1/accountmanager/lock" + }; + } + + rpc Generate(GenerateRequest) returns (GenerateResponse) { + option (google.api.http) = { + post: "/v1/accountmanager/generate" + }; + } +} + +message UnlockAccountRequest { + string account = 1; + bytes passphrase = 2; +} + +message LockAccountRequest { + string account = 1; +} + +message UnlockAccountResponse { + ResponseState state = 1; +} + +message LockAccountResponse { + ResponseState state = 1; +} + +message GenerateRequest { + string account = 1; + bytes passphrase = 2; + uint32 participants = 3; + uint32 signing_threshold = 4; +} + +message GenerateResponse { + ResponseState state = 1; + string message = 2; + bytes public_key = 3; + repeated Endpoint participants = 4; +} diff --git a/crates/signer/proto/dirk/dkg.proto b/crates/signer/proto/dirk/dkg.proto new file mode 100644 index 00000000..e335a4b7 --- /dev/null +++ b/crates/signer/proto/dirk/dkg.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package v1; + +import "google/protobuf/empty.proto"; +import "endpoint.proto"; + +option csharp_namespace = "Eth2Signer.v1"; +option php_namespace = "Eth2Signer\\v1"; +option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; +option java_package = "com.wealdtech.eth2signerapi.v1"; +option java_multiple_files = true; +option java_outer_classname = "DKGProto"; + +// DKG is the internal protocol that runs between distributed key generators. +service DKG { + rpc Prepare(PrepareRequest) returns (google.protobuf.Empty) { } + rpc Execute(ExecuteRequest) returns (google.protobuf.Empty) { } + rpc Commit(CommitRequest) returns (CommitResponse) { } + rpc Abort(AbortRequest) returns (google.protobuf.Empty) { } + rpc Contribute(ContributeRequest) returns (ContributeResponse) { } +} + +message PrepareRequest { + // account is the name of the account. + string account = 1; + // threshold is the number of participants required to generate a valid signature. + uint32 threshold = 2; + // participants contains the endpoints of all participants. + repeated Endpoint participants = 3; + // passphrase is the passphrase of the account. + bytes passphrase = 4; +} + +message ExecuteRequest { + // account is the name of the account. + string account = 1; +} + +message CommitRequest { + // account is the name of the account. + string account = 1; + // confirmation data is data used to generate the confirmation signature. + bytes confirmation_data = 2; +} + +message CommitResponse { + // public_key is the key generated by the process. + bytes public_key = 1; + // confirmation_signature is the signature generated by the individual secret key. + bytes confirmation_signature = 2; +} + +message AbortRequest { + // account is the name of the account. + string account = 1; +} + +// ContributeRequest is sent by each part to all other parties with a contribution. +message ContributeRequest { + string account = 1; + bytes secret = 2; + repeated bytes verification_vector = 3; +} + +// ContributeResponse receives the contribution from a participant. +message ContributeResponse { + bytes secret = 1; + repeated bytes verification_vector = 2; +} diff --git a/crates/signer/proto/dirk/endpoint.proto b/crates/signer/proto/dirk/endpoint.proto new file mode 100644 index 00000000..e61f84ae --- /dev/null +++ b/crates/signer/proto/dirk/endpoint.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package v1; + +option csharp_namespace = "Eth2Signer.v1"; +option php_namespace = "Eth2Signer\\v1"; +option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; +option java_package = "com.wealdtech.eth2signerapi.v1"; +option java_multiple_files = true; +option java_outer_classname = "EndpointProto"; + +message Endpoint { + uint64 id = 1; + string name = 2; + uint32 port = 3; +} diff --git a/crates/signer/proto/dirk/eth2.proto b/crates/signer/proto/dirk/eth2.proto new file mode 100644 index 00000000..615ba814 --- /dev/null +++ b/crates/signer/proto/dirk/eth2.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package v1; + +option csharp_namespace = "Eth2Signer.v1"; +option php_namespace = "Eth2Signer\\v1"; +option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; +option java_package = "com.wealdtech.eth2signerapi.v1"; +option java_multiple_files = true; +option java_outer_classname = "Eth2Proto"; + +// AttestationData is defined at https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md#attestationdata +message AttestationData { + uint64 slot = 1; + uint64 committee_index = 2; + bytes beacon_block_root = 3; + Checkpoint source = 4; + Checkpoint target = 5; +} + +// Checkpoint is defined at https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md#checkpoint +message Checkpoint { + uint64 epoch = 1; + bytes root = 2; +} + +// BeaconBlockheader is defined at https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +message BeaconBlockHeader { + uint64 slot = 1; + uint64 proposer_index = 2; + bytes parent_root = 3; + bytes state_root = 4; + bytes body_root = 5; +} diff --git a/crates/signer/proto/dirk/lister.proto b/crates/signer/proto/dirk/lister.proto new file mode 100644 index 00000000..b3d6642b --- /dev/null +++ b/crates/signer/proto/dirk/lister.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +package v1; + +import "google/api/annotations.proto"; +import "endpoint.proto"; +import "responsestate.proto"; + +option csharp_namespace = "Eth2Signer.v1"; +option php_namespace = "Eth2Signer\\v1"; +option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; +option java_package = "com.wealdtech.eth2signerapi.v1"; +option java_multiple_files = true; +option java_outer_classname = "ListerProto"; + +service Lister { + rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) { + option (google.api.http) = { + get: "/v1/lister/listaccounts" + }; + } +} + +message ListAccountsRequest { + repeated string paths = 1; +} + +message ListAccountsResponse { + ResponseState state = 1; + repeated Account Accounts = 2; + repeated DistributedAccount DistributedAccounts = 3; +} + +message Account { + string name = 1; + bytes public_key = 2; + bytes uuid = 3; +} + +message DistributedAccount { + string name = 1; + bytes public_key = 2; + repeated Endpoint participants = 3; + uint32 signing_threshold = 4; + bytes uuid = 5; + bytes composite_public_key = 6; +} diff --git a/crates/signer/proto/dirk/responsestate.proto b/crates/signer/proto/dirk/responsestate.proto new file mode 100644 index 00000000..8eb57d9e --- /dev/null +++ b/crates/signer/proto/dirk/responsestate.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package v1; + +option csharp_namespace = "Eth2Signer.v1"; +option php_namespace = "Eth2Signer\\v1"; +option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; +option java_package = "com.wealdtech.eth2signerapi.v1"; +option java_multiple_files = true; +option java_outer_classname = "ResponseStateProto"; + +enum ResponseState { + // UNKNOWN occurs when no information about the response is available. + UNKNOWN = 0; + // SUCCEEDED occurs when a request was successful. + SUCCEEDED = 1; + // DENIED occurs when a request was denied. + DENIED = 2; + // FAILED occurs when a request failed to complete. + FAILED = 3; +} diff --git a/crates/signer/proto/dirk/signer.proto b/crates/signer/proto/dirk/signer.proto new file mode 100644 index 00000000..02612063 --- /dev/null +++ b/crates/signer/proto/dirk/signer.proto @@ -0,0 +1,86 @@ +syntax = "proto3"; + +package v1; + +import "google/api/annotations.proto"; +import "eth2.proto"; +import "responsestate.proto"; + +option csharp_namespace = "Eth2Signer.v1"; +option php_namespace = "Eth2Signer\\v1"; +option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; +option java_package = "com.wealdtech.eth2signerapi.v1"; +option java_multiple_files = true; +option java_outer_classname = "SignerProto"; + +service Signer { + rpc Sign(SignRequest) returns (SignResponse) { + option (google.api.http) = { + get: "/v1/signer/sign" + }; + } + rpc Multisign(MultisignRequest) returns (MultisignResponse) { + option (google.api.http) = { + get: "/v1/signer/multisign" + }; + } + rpc SignBeaconAttestation(SignBeaconAttestationRequest) returns (SignResponse) { + option (google.api.http) = { + get: "/v1/signer/signbeaconattestation" + }; + } + rpc SignBeaconAttestations(SignBeaconAttestationsRequest) returns (MultisignResponse) { + option (google.api.http) = { + get: "/v1/signer/signbeaconattestations" + }; + } + rpc SignBeaconProposal(SignBeaconProposalRequest) returns (SignResponse) { + option (google.api.http) = { + get: "/v1/signer/signbeaconproposal" + }; + } +} + +message SignRequest { + oneof id { + bytes public_key = 1; + string account = 2; + } + bytes data = 3; + bytes domain = 4; +} + +message MultisignRequest { + repeated SignRequest requests = 1; +} + +message SignBeaconAttestationRequest { + oneof id { + bytes public_key = 1; + string account = 2; + } + bytes domain = 3; + AttestationData data = 4; +} + +message SignBeaconAttestationsRequest { + repeated SignBeaconAttestationRequest requests = 1; +} + +message SignBeaconProposalRequest { + oneof id { + bytes public_key = 1; + string account = 2; + } + bytes domain = 3; + BeaconBlockHeader data = 4; +} + +message SignResponse { + ResponseState state = 1; + bytes signature = 2; +} + +message MultisignResponse { + repeated SignResponse responses = 1; +} diff --git a/crates/signer/proto/dirk/walletmanager.proto b/crates/signer/proto/dirk/walletmanager.proto new file mode 100644 index 00000000..98a5d8a0 --- /dev/null +++ b/crates/signer/proto/dirk/walletmanager.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package v1; + +import "google/api/annotations.proto"; +import "responsestate.proto"; + +option csharp_namespace = "Eth2Signer.v1"; +option php_namespace = "Eth2Signer\\v1"; +option java_package = "com.wealdtech.eth2signerapi.v1"; +option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; +option java_multiple_files = true; +option java_outer_classname = "WalletManagerProto"; + +service WalletManager { + rpc Unlock(UnlockWalletRequest) returns (UnlockWalletResponse) { + option (google.api.http) = { + get: "/v1/walletmanager/unlock" + }; + } + rpc Lock(LockWalletRequest) returns (LockWalletResponse) { + option (google.api.http) = { + get: "/v1/walletmanager/lock" + }; + } +} + +message UnlockWalletRequest { + string wallet = 1; + bytes passphrase = 2; +} + +message LockWalletRequest { + string wallet = 1; +} + +message UnlockWalletResponse { + ResponseState state = 1; +} + +message LockWalletResponse { + ResponseState state = 1; +} diff --git a/crates/signer/proto/third-party/google/api/annotations.proto b/crates/signer/proto/third-party/google/api/annotations.proto new file mode 100644 index 00000000..85c361b4 --- /dev/null +++ b/crates/signer/proto/third-party/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright (c) 2015, Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/crates/signer/proto/third-party/google/api/http.proto b/crates/signer/proto/third-party/google/api/http.proto new file mode 100644 index 00000000..2bd3a19b --- /dev/null +++ b/crates/signer/proto/third-party/google/api/http.proto @@ -0,0 +1,318 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parmeters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// `HttpRule` defines the mapping of an RPC method to one or more HTTP +// REST API methods. The mapping specifies how different portions of the RPC +// request message are mapped to URL path, URL query parameters, and +// HTTP request body. The mapping is typically specified as an +// `google.api.http` annotation on the RPC method, +// see "google/api/annotations.proto" for details. +// +// The mapping consists of a field specifying the path template and +// method kind. The path template can refer to fields in the request +// message, as in the example below which describes a REST GET +// operation on a resource collection of messages: +// +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}"; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // mapped to the URL +// SubMessage sub = 2; // `sub.subfield` is url-mapped +// } +// message Message { +// string text = 1; // content of the resource +// } +// +// The same http annotation can alternatively be expressed inside the +// `GRPC API Configuration` YAML file. +// +// http: +// rules: +// - selector: .Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// This definition enables an automatic, bidrectional mapping of HTTP +// JSON to RPC. Example: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))` +// +// In general, not only fields but also field paths can be referenced +// from a path pattern. Fields mapped to the path pattern cannot be +// repeated and must have a primitive (non-message) type. +// +// Any fields in the request message which are not bound by the path +// pattern automatically become (optional) HTTP query +// parameters. Assume the following definition of the request message: +// +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http).get = "/v1/messages/{message_id}"; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // mapped to the URL +// int64 revision = 2; // becomes a parameter +// SubMessage sub = 3; // `sub.subfield` becomes a parameter +// } +// +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))` +// +// Note that fields which are mapped to HTTP parameters must have a +// primitive type or a repeated primitive type. Message types are not +// allowed. In the case of a repeated type, the parameter can be +// repeated in the URL, as in `...?param=A¶m=B`. +// +// For HTTP method kinds which allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// put: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | RPC +// -----|----- +// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// put: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | RPC +// -----|----- +// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice of +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// +// This enables the following two alternative HTTP JSON to RPC +// mappings: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")` +// +// # Rules for HTTP mapping +// +// The rules for mapping HTTP path, query parameters, and body fields +// to the request message are as follows: +// +// 1. The `body` field specifies either `*` or a field path, or is +// omitted. If omitted, it indicates there is no HTTP request body. +// 2. Leaf fields (recursive expansion of nested messages in the +// request) can be classified into three types: +// (a) Matched in the URL template. +// (b) Covered by body (if body is `*`, everything except (a) fields; +// else everything under the body field) +// (c) All other fields. +// 3. URL query parameters found in the HTTP request are mapped to (c) fields. +// 4. Any body sent with an HTTP request can contain only (b) fields. +// +// The syntax of the path template is as follows: +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single path segment. The syntax `**` matches zero +// or more path segments, which must be the last part of the path except the +// `Verb`. The syntax `LITERAL` matches literal text in the path. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path, all characters +// except `[-_.~0-9a-zA-Z]` are percent-encoded. Such variables show up in the +// Discovery Document as `{var}`. +// +// If a variable contains one or more path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path, all +// characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. Such variables +// show up in the Discovery Document as `{+var}`. +// +// NOTE: While the single segment variable matches the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 +// Simple String Expansion, the multi segment variable **does not** match +// RFC 6570 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. +// +// NOTE: the field paths in variables and in the `body` must not refer to +// repeated fields or map fields. +message HttpRule { + // Selects methods to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Used for listing and getting information about resources. + string get = 2; + + // Used for updating a resource. + string put = 3; + + // Used for creating a resource. + string post = 4; + + // Used for deleting a resource. + string delete = 5; + + // Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP body, or + // `*` for mapping all fields not captured by the path pattern to the HTTP + // body. NOTE: the referred field must not be a repeated field and must be + // present at the top-level of request message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // body of response. Other response fields are ignored. When + // not set, the response message will be used as HTTP body of response. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/crates/signer/src/dirk.rs b/crates/signer/src/dirk.rs new file mode 100644 index 00000000..a6a84e03 --- /dev/null +++ b/crates/signer/src/dirk.rs @@ -0,0 +1,68 @@ +use alloy::{primitives::FixedBytes, transports::http::reqwest::Url}; +use cb_common::{commit::request::ConsensusProxyMap, signer::BlsPublicKey, types::ModuleId}; +use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; + +use v1::{lister_client::ListerClient, ListAccountsRequest, ResponseState}; + +pub mod v1 { + tonic::include_proto!("v1"); +} + +#[derive(Clone)] +pub struct DirkConfig { + pub url: Url, + pub client_cert: Identity, + pub cert_auth: Option, + pub server_domain: Option, +} + +#[derive(Clone)] +pub struct DirkClient { + channel: Channel, +} + +impl DirkClient { + pub async fn new_from_config(config: DirkConfig) -> Result { + let mut tls_config = ClientTlsConfig::new().identity(config.client_cert); + + if let Some(ca) = config.cert_auth { + tls_config = tls_config.ca_certificate(ca); + } + + if let Some(server_domain) = config.server_domain { + tls_config = tls_config.domain_name(server_domain); + } + + let channel = Channel::from_shared(config.url.to_string()) + .map_err(|_| eyre::eyre!("Invalid Dirk URL".to_string()))? + .tls_config(tls_config) + .map_err(|_| eyre::eyre!("Invalid Dirk URL".to_string()))? + .connect() + .await + .map_err(|e| eyre::eyre!(format!("Couldn't connect to Dirk: {e}")))?; + + Ok(Self { channel }) + } + + pub async fn get_pubkeys( + &self, + module_id: ModuleId, + ) -> Result, eyre::Error> { + let mut client = ListerClient::new(self.channel.clone()); + let dirk_request = + tonic::Request::new(ListAccountsRequest { paths: vec![module_id.to_string()] }); + let dirk_response = client.list_accounts(dirk_request).await.unwrap(); + + if dirk_response.get_ref().state() != ResponseState::Succeeded { + return Err(eyre::eyre!("Dirk request failed".to_string())); + } + + let mut keys = Vec::new(); + for account in dirk_response.into_inner().accounts.iter() { + keys.push(ConsensusProxyMap::new(BlsPublicKey::from(FixedBytes::from_slice( + &account.public_key, + )))) + } + Ok(keys) + } +} diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 84bd08d9..df3408c5 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -1,4 +1,5 @@ mod constants; +mod dirk; pub mod error; pub mod manager; mod metrics; diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 228158e9..bb7eeb15 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -1,5 +1,6 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::{net::SocketAddr, str::FromStr, sync::Arc}; +use alloy::transports::http::reqwest::Url; use axum::{ extract::{Request, State}, http::StatusCode, @@ -32,11 +33,14 @@ use tracing::{debug, error, info, warn}; use uuid::Uuid; use crate::{ + dirk::{DirkClient, DirkConfig}, error::SignerModuleError, manager::SigningManager, metrics::{uri_to_tag, SIGNER_METRICS_REGISTRY, SIGNER_STATUS}, }; +use tonic::transport::{Certificate, Identity}; + /// Implements the Signer API and provides a service for signing requests pub struct SigningService; @@ -47,6 +51,8 @@ struct SigningState { /// Map of JWTs to module ids. This also acts as registry of all modules /// running jwts: Arc>, + /// Dirk settings + dirk: Option, } impl SigningService { @@ -76,7 +82,25 @@ impl SigningService { info!(version = COMMIT_BOOST_VERSION, modules =? module_ids, port =? config.server_port, loaded_consensus, loaded_proxies, "Starting signing service"); - let state = SigningState { manager: RwLock::new(manager).into(), jwts: config.jwts.into() }; + let state = SigningState { + manager: RwLock::new(manager).into(), + jwts: config.jwts.into(), + dirk: Some( + DirkClient::new_from_config(DirkConfig { + url: Url::from_str("https://localhost:9091").unwrap(), + client_cert: Identity::from_pem( + std::fs::read_to_string("client1.crt").unwrap(), + std::fs::read_to_string("client1.key").unwrap(), + ), + cert_auth: Some(Certificate::from_pem( + std::fs::read_to_string("dirk_authority.crt").unwrap(), + )), + server_domain: Some("server.example.com".into()), + }) + .await + .unwrap(), + ), + }; SigningService::init_metrics()?; let app = axum::Router::new() @@ -142,12 +166,16 @@ async fn handle_get_pubkeys( debug!(event = "get_pubkeys", ?req_id, "New request"); - let signing_manager = state.manager.read().await; - let map = signing_manager - .get_consensus_proxy_maps(&module_id) - .map_err(|err| SignerModuleError::Internal(err.to_string()))?; + let keys = if let Some(dirk) = state.dirk { + dirk.get_pubkeys(module_id).await.map_err(|e| SignerModuleError::Internal(e.to_string()))? + } else { + let signing_manager = state.manager.read().await; + signing_manager + .get_consensus_proxy_maps(&module_id) + .map_err(|err| SignerModuleError::Internal(err.to_string()))? + }; - let res = GetPubkeysResponse { keys: map }; + let res = GetPubkeysResponse { keys }; Ok((StatusCode::OK, Json(res)).into_response()) } From 072ed64b1f6bec13b05f1a4f8b27477b3689cf07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 30 Dec 2024 19:31:24 -0300 Subject: [PATCH 002/104] Message signing --- crates/cli/src/docker_init.rs | 7 ++- crates/common/Cargo.toml | 7 +++ crates/common/src/config/signer.rs | 54 ++++++++++++++++-- crates/signer/build.rs | 7 ++- crates/signer/src/dirk.rs | 69 +++++++++++++++++------ crates/signer/src/service.rs | 90 +++++++++++++++--------------- provisioning/pbs.Dockerfile | 1 + provisioning/signer.Dockerfile | 1 + 8 files changed, 164 insertions(+), 72 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 5d257365..f4bb82a3 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -156,9 +156,10 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu // depends_on let mut module_dependencies = IndexMap::new(); - module_dependencies.insert("cb_signer".into(), DependsCondition { - condition: "service_healthy".into(), - }); + module_dependencies.insert( + "cb_signer".into(), + DependsCondition { condition: "service_healthy".into() }, + ); Service { container_name: Some(module_cid.clone()), diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 5cb8a410..ce64b7da 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -51,3 +51,10 @@ derive_more.workspace = true unicode-normalization.workspace = true base64.workspace = true + +tonic = { version = "0.12.3", features = [ + "tls", + "channel", + "prost", + "codegen", +] } diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 6f38a800..60de395e 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -1,7 +1,11 @@ +use std::path::PathBuf; + +use alloy::transports::http::reqwest::Url; use bimap::BiHashMap; use eyre::{bail, Result}; use serde::{Deserialize, Serialize}; -use url::Url; +use tonic::transport::{Certificate, Identity}; +use tracing::info; use super::{ constants::SIGNER_IMAGE_DEFAULT, @@ -26,36 +30,76 @@ pub enum SignerConfig { /// How to store keys store: Option, }, - /// Remote signer module with compatible API + /// Remote signer module with compatible API like Web3Signer Remote { /// Complete url of the base API endpoint url: Url, }, + Dirk { + url: Url, + cert_path: PathBuf, + key_path: PathBuf, + wallet: String, + }, } fn default_signer() -> String { SIGNER_IMAGE_DEFAULT.to_string() } +#[derive(Clone, Debug)] +pub struct DirkConfig { + pub url: Url, + pub client_cert: Identity, + pub cert_auth: Option, + pub server_domain: Option, +} + #[derive(Debug)] pub struct StartSignerConfig { pub chain: Chain, - pub loader: SignerLoader, + pub loader: Option, pub store: Option, pub server_port: u16, pub jwts: BiHashMap, + pub dirk: Option, } impl StartSignerConfig { pub fn load_from_env() -> Result { + info!("Loading from env"); let config = CommitBoostConfig::from_env_path()?; let jwts = load_jwts()?; let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?; match config.signer { - Some(SignerConfig::Local { loader, store, .. }) => { - Ok(StartSignerConfig { chain: config.chain, loader, server_port, jwts, store }) + Some(SignerConfig::Local { loader, store, .. }) => Ok(StartSignerConfig { + chain: config.chain, + loader: Some(loader), + server_port, + jwts, + store, + dirk: None, + }), + Some(SignerConfig::Dirk { url, cert_path, key_path, wallet: _ }) => { + Ok(StartSignerConfig { + chain: config.chain, + loader: None, + server_port, + jwts, + store: None, + dirk: Some(DirkConfig { + url, + client_cert: Identity::from_pem( + std::fs::read_to_string(cert_path)?, + std::fs::read_to_string(key_path)?, + ), + cert_auth: None, + server_domain: None, + }), + }) + // Ok(StartSignerConfig {chain: config.chain, server_port, jwts, dirk}) } Some(SignerConfig::Remote { .. }) => bail!("Remote signer configured"), None => bail!("Signer config is missing"), diff --git a/crates/signer/build.rs b/crates/signer/build.rs index 3407eacf..c97d3558 100644 --- a/crates/signer/build.rs +++ b/crates/signer/build.rs @@ -1,6 +1,7 @@ fn main() -> Result<(), Box> { - tonic_build::configure() - .build_server(false) - .compile_protos(&["proto/dirk/lister.proto"], &["proto/dirk", "proto/third-party"])?; + tonic_build::configure().build_server(false).compile_protos( + &["proto/dirk/lister.proto", "proto/dirk/accountmanager.proto", "proto/dirk/signer.proto"], + &["proto/dirk", "proto/third-party"], + )?; Ok(()) } diff --git a/crates/signer/src/dirk.rs b/crates/signer/src/dirk.rs index a6a84e03..b458b13f 100644 --- a/crates/signer/src/dirk.rs +++ b/crates/signer/src/dirk.rs @@ -1,22 +1,23 @@ -use alloy::{primitives::FixedBytes, transports::http::reqwest::Url}; -use cb_common::{commit::request::ConsensusProxyMap, signer::BlsPublicKey, types::ModuleId}; -use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; +use alloy::{hex, primitives::FixedBytes}; +use cb_common::{ + commit::request::ConsensusProxyMap, + config::DirkConfig, + signer::{BlsPublicKey, BlsSignature}, + types::ModuleId, +}; +use tonic::transport::{Channel, ClientTlsConfig}; -use v1::{lister_client::ListerClient, ListAccountsRequest, ResponseState}; +use v1::{ + account_manager_client::AccountManagerClient, lister_client::ListerClient, + sign_request::Id as SignerId, signer_client::SignerClient, ListAccountsRequest, ResponseState, + UnlockAccountRequest, +}; pub mod v1 { tonic::include_proto!("v1"); } -#[derive(Clone)] -pub struct DirkConfig { - pub url: Url, - pub client_cert: Identity, - pub cert_auth: Option, - pub server_domain: Option, -} - -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct DirkClient { channel: Channel, } @@ -49,12 +50,13 @@ impl DirkClient { module_id: ModuleId, ) -> Result, eyre::Error> { let mut client = ListerClient::new(self.channel.clone()); - let dirk_request = - tonic::Request::new(ListAccountsRequest { paths: vec![module_id.to_string()] }); + let dirk_request = tonic::Request::new(ListAccountsRequest { + paths: vec!["wallet1/account1".to_string()], + }); let dirk_response = client.list_accounts(dirk_request).await.unwrap(); if dirk_response.get_ref().state() != ResponseState::Succeeded { - return Err(eyre::eyre!("Dirk request failed".to_string())); + eyre::bail!("Dirk request failed".to_string()); } let mut keys = Vec::new(); @@ -65,4 +67,39 @@ impl DirkClient { } Ok(keys) } + + pub async fn request_signature( + &self, + id: SignerId, + object_root: [u8; 32], + ) -> Result { + let mut unlock_client = AccountManagerClient::new(self.channel.clone()); + let unlock_request = tonic::Request::new(UnlockAccountRequest { + account: match &id { + SignerId::Account(account) => account.to_string(), + SignerId::PublicKey(_) => unimplemented!(), + }, + passphrase: hex::decode("736563726574").unwrap(), + }); + + let unlock_response = unlock_client.unlock(unlock_request).await.unwrap(); + if unlock_response.get_ref().state() != ResponseState::Succeeded { + eyre::bail!("Unlock request failed"); + } + + let mut signer_client = SignerClient::new(self.channel.clone()); + let sign_request = tonic::Request::new(v1::SignRequest { + id: Some(id), + data: object_root.to_vec(), + domain: hex::decode("1ea343e3a29ec9b8de4981dc755220fa10635c6e2ad13a0df6abfc5663c66a88") + .unwrap(), + }); + + let sign_response = signer_client.sign(sign_request).await.unwrap(); + if sign_response.get_ref().state() != ResponseState::Succeeded { + eyre::bail!("Sign request failed"); + } + + Ok(BlsSignature::from(FixedBytes::from_slice(&sign_response.into_inner().signature))) + } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index bb7eeb15..054f1097 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -1,6 +1,5 @@ -use std::{net::SocketAddr, str::FromStr, sync::Arc}; +use std::{net::SocketAddr, sync::Arc}; -use alloy::transports::http::reqwest::Url; use axum::{ extract::{Request, State}, http::StatusCode, @@ -33,14 +32,12 @@ use tracing::{debug, error, info, warn}; use uuid::Uuid; use crate::{ - dirk::{DirkClient, DirkConfig}, + dirk::{v1::sign_request::Id as SignerId, DirkClient}, error::SignerModuleError, manager::SigningManager, metrics::{uri_to_tag, SIGNER_METRICS_REGISTRY, SIGNER_STATUS}, }; -use tonic::transport::{Certificate, Identity}; - /// Implements the Signer API and provides a service for signing requests pub struct SigningService; @@ -71,8 +68,10 @@ impl SigningService { let mut manager = SigningManager::new(config.chain, proxy_store)?; - for signer in config.loader.load_keys()? { - manager.add_consensus_signer(signer); + if let Some(loader) = config.loader { + for signer in loader.load_keys()? { + manager.add_consensus_signer(signer); + } } let module_ids: Vec = config.jwts.left_values().cloned().map(Into::into).collect(); @@ -85,21 +84,10 @@ impl SigningService { let state = SigningState { manager: RwLock::new(manager).into(), jwts: config.jwts.into(), - dirk: Some( - DirkClient::new_from_config(DirkConfig { - url: Url::from_str("https://localhost:9091").unwrap(), - client_cert: Identity::from_pem( - std::fs::read_to_string("client1.crt").unwrap(), - std::fs::read_to_string("client1.key").unwrap(), - ), - cert_auth: Some(Certificate::from_pem( - std::fs::read_to_string("dirk_authority.crt").unwrap(), - )), - server_domain: Some("server.example.com".into()), - }) - .await - .unwrap(), - ), + dirk: match config.dirk { + Some(dirk) => Some(DirkClient::new_from_config(dirk).await?), + None => None, + }, }; SigningService::init_metrics()?; @@ -192,34 +180,46 @@ async fn handle_request_signature( let signing_manager = state.manager.read().await; - let signature_response = match request { - SignRequest::Consensus(SignConsensusRequest { pubkey, object_root }) => signing_manager - .sign_consensus(&pubkey, &object_root) + let object_root = match request { + SignRequest::Consensus(SignConsensusRequest { object_root, .. }) => object_root, + SignRequest::ProxyBls(SignProxyRequest { object_root, .. }) => object_root, + SignRequest::ProxyEcdsa(SignProxyRequest { object_root, .. }) => object_root, + }; + let signature_response = if let Some(dirk) = state.dirk { + dirk.request_signature(SignerId::Account("wallet1/account1".to_string()), object_root) .await - .map(|sig| Json(sig).into_response()), - SignRequest::ProxyBls(SignProxyRequest { pubkey: bls_pk, object_root }) => { - if !signing_manager.has_proxy_bls_for_module(&bls_pk, &module_id) { - return Err(SignerModuleError::UnknownProxySigner(bls_pk.to_vec())); - } - - signing_manager - .sign_proxy_bls(&bls_pk, &object_root) + .map(|sig| Json(sig).into_response()) + .map_err(|e| SignerModuleError::Internal(e.to_string())) + } else { + match request { + SignRequest::Consensus(SignConsensusRequest { pubkey, object_root }) => signing_manager + .sign_consensus(&pubkey, &object_root) .await - .map(|sig| Json(sig).into_response()) - } - SignRequest::ProxyEcdsa(SignProxyRequest { pubkey: ecdsa_pk, object_root }) => { - if !signing_manager.has_proxy_ecdsa_for_module(&ecdsa_pk, &module_id) { - return Err(SignerModuleError::UnknownProxySigner(ecdsa_pk.to_vec())); + .map(|sig| Json(sig).into_response()), + SignRequest::ProxyBls(SignProxyRequest { pubkey: bls_pk, object_root }) => { + if !signing_manager.has_proxy_bls_for_module(&bls_pk, &module_id) { + return Err(SignerModuleError::UnknownProxySigner(bls_pk.to_vec())); + } + + signing_manager + .sign_proxy_bls(&bls_pk, &object_root) + .await + .map(|sig| Json(sig).into_response()) + } + SignRequest::ProxyEcdsa(SignProxyRequest { pubkey: ecdsa_pk, object_root }) => { + if !signing_manager.has_proxy_ecdsa_for_module(&ecdsa_pk, &module_id) { + return Err(SignerModuleError::UnknownProxySigner(ecdsa_pk.to_vec())); + } + + signing_manager + .sign_proxy_ecdsa(&ecdsa_pk, &object_root) + .await + .map(|sig| Json(sig).into_response()) } - - signing_manager - .sign_proxy_ecdsa(&ecdsa_pk, &object_root) - .await - .map(|sig| Json(sig).into_response()) } - }?; + }; - Ok(signature_response) + Ok(signature_response?) } async fn handle_generate_proxy( diff --git a/provisioning/pbs.Dockerfile b/provisioning/pbs.Dockerfile index 9d2a2fcf..cf142622 100644 --- a/provisioning/pbs.Dockerfile +++ b/provisioning/pbs.Dockerfile @@ -8,6 +8,7 @@ RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json +RUN apt-get update && apt-get install -y protobuf-compiler RUN cargo chef cook --release --recipe-path recipe.json COPY . . diff --git a/provisioning/signer.Dockerfile b/provisioning/signer.Dockerfile index 3f267806..9872e570 100644 --- a/provisioning/signer.Dockerfile +++ b/provisioning/signer.Dockerfile @@ -8,6 +8,7 @@ RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json +RUN apt-get update && apt-get install -y protobuf-compiler RUN cargo chef cook --release --recipe-path recipe.json COPY . . From a6f55f5b52dc16d25c4ca15f88972fbe8f319e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 2 Jan 2025 15:50:42 -0300 Subject: [PATCH 003/104] Implement docker setup --- Cargo.lock | 1 + crates/cli/src/docker_init.rs | 293 +++++++++++++++++--------- crates/common/src/config/constants.rs | 7 + crates/common/src/config/signer.rs | 51 ++++- 4 files changed, 246 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c3c25fb..74db47b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1328,6 +1328,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "toml", + "tonic", "tracing", "tracing-appender", "tracing-subscriber", diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index f4bb82a3..eec13acc 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -11,6 +11,8 @@ use cb_common::{ LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, + SIGNER_DIRK_CA_CERT_DEFAULT, SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CERT_DEFAULT, + SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_KEY_DEFAULT, SIGNER_DIRK_KEY_ENV, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_URL_ENV, @@ -313,131 +315,224 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu services.insert("cb_pbs".to_owned(), Some(pbs_service)); // setup signer service - if let Some(SignerConfig::Local { docker_image, loader, store }) = cb_config.signer { - if needs_signer_module { - if metrics_enabled { - targets.push(PrometheusTargetConfig { - targets: vec![format!("cb_signer:{metrics_port}")], - labels: PrometheusLabelsConfig { job: "signer".into() }, - }); - } - - let mut signer_envs = IndexMap::from([ - get_env_val(CONFIG_ENV, CONFIG_DEFAULT), - get_env_same(JWTS_ENV), - get_env_uval(SIGNER_PORT_ENV, signer_port as u64), - ]); - - if let Some((key, val)) = chain_spec_env.clone() { - signer_envs.insert(key, val); - } - if metrics_enabled { - let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); - signer_envs.insert(key, val); - } - if log_to_file { - let (key, val) = get_env_val(LOGS_DIR_ENV, LOGS_DIR_DEFAULT); - signer_envs.insert(key, val); - } - - // write jwts to env - envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts)); + match cb_config.signer { + Some(SignerConfig::Local { docker_image, loader, store }) => { + if needs_signer_module { + if metrics_enabled { + targets.push(PrometheusTargetConfig { + targets: vec![format!("cb_signer:{metrics_port}")], + labels: PrometheusLabelsConfig { job: "signer".into() }, + }); + } - // volumes - let mut volumes = vec![config_volume.clone()]; - volumes.extend(chain_spec_volume.clone()); + let mut signer_envs = IndexMap::from([ + get_env_val(CONFIG_ENV, CONFIG_DEFAULT), + get_env_same(JWTS_ENV), + get_env_uval(SIGNER_PORT_ENV, signer_port as u64), + ]); - match loader { - SignerLoader::File { key_path } => { - volumes.push(Volumes::Simple(format!( - "{}:{}:ro", - key_path.display(), - SIGNER_DEFAULT - ))); - let (k, v) = get_env_val(SIGNER_KEYS_ENV, SIGNER_DEFAULT); - signer_envs.insert(k, v); + if let Some((key, val)) = chain_spec_env.clone() { + signer_envs.insert(key, val); } - SignerLoader::ValidatorsDir { keys_path, secrets_path, format: _ } => { - volumes.push(Volumes::Simple(format!( - "{}:{}:ro", - keys_path.display(), - SIGNER_DIR_KEYS_DEFAULT - ))); - let (k, v) = get_env_val(SIGNER_DIR_KEYS_ENV, SIGNER_DIR_KEYS_DEFAULT); - signer_envs.insert(k, v); - - volumes.push(Volumes::Simple(format!( - "{}:{}:ro", - secrets_path.display(), - SIGNER_DIR_SECRETS_DEFAULT - ))); - let (k, v) = get_env_val(SIGNER_DIR_SECRETS_ENV, SIGNER_DIR_SECRETS_DEFAULT); - signer_envs.insert(k, v); + if metrics_enabled { + let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); + signer_envs.insert(key, val); + } + if log_to_file { + let (key, val) = get_env_val(LOGS_DIR_ENV, LOGS_DIR_DEFAULT); + signer_envs.insert(key, val); } - }; - if let Some(store) = store { - match store { - ProxyStore::File { proxy_dir } => { + // write jwts to env + envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts)); + + // volumes + let mut volumes = vec![config_volume.clone()]; + volumes.extend(chain_spec_volume.clone()); + + match loader { + SignerLoader::File { key_path } => { volumes.push(Volumes::Simple(format!( - "{}:{}:rw", - proxy_dir.display(), - PROXY_DIR_DEFAULT + "{}:{}:ro", + key_path.display(), + SIGNER_DEFAULT ))); - let (k, v) = get_env_val(PROXY_DIR_ENV, PROXY_DIR_DEFAULT); + let (k, v) = get_env_val(SIGNER_KEYS_ENV, SIGNER_DEFAULT); signer_envs.insert(k, v); } - ProxyStore::ERC2335 { keys_path, secrets_path } => { + SignerLoader::ValidatorsDir { keys_path, secrets_path, format: _ } => { volumes.push(Volumes::Simple(format!( - "{}:{}:rw", + "{}:{}:ro", keys_path.display(), - PROXY_DIR_KEYS_DEFAULT + SIGNER_DIR_KEYS_DEFAULT ))); - let (k, v) = get_env_val(PROXY_DIR_KEYS_ENV, PROXY_DIR_KEYS_DEFAULT); + let (k, v) = get_env_val(SIGNER_DIR_KEYS_ENV, SIGNER_DIR_KEYS_DEFAULT); signer_envs.insert(k, v); volumes.push(Volumes::Simple(format!( - "{}:{}:rw", + "{}:{}:ro", secrets_path.display(), - PROXY_DIR_SECRETS_DEFAULT + SIGNER_DIR_SECRETS_DEFAULT ))); - let (k, v) = get_env_val(PROXY_DIR_SECRETS_ENV, PROXY_DIR_SECRETS_DEFAULT); + let (k, v) = + get_env_val(SIGNER_DIR_SECRETS_ENV, SIGNER_DIR_SECRETS_DEFAULT); signer_envs.insert(k, v); } + }; + + if let Some(store) = store { + match store { + ProxyStore::File { proxy_dir } => { + volumes.push(Volumes::Simple(format!( + "{}:{}:rw", + proxy_dir.display(), + PROXY_DIR_DEFAULT + ))); + let (k, v) = get_env_val(PROXY_DIR_ENV, PROXY_DIR_DEFAULT); + signer_envs.insert(k, v); + } + ProxyStore::ERC2335 { keys_path, secrets_path } => { + volumes.push(Volumes::Simple(format!( + "{}:{}:rw", + keys_path.display(), + PROXY_DIR_KEYS_DEFAULT + ))); + let (k, v) = get_env_val(PROXY_DIR_KEYS_ENV, PROXY_DIR_KEYS_DEFAULT); + signer_envs.insert(k, v); + + volumes.push(Volumes::Simple(format!( + "{}:{}:rw", + secrets_path.display(), + PROXY_DIR_SECRETS_DEFAULT + ))); + let (k, v) = + get_env_val(PROXY_DIR_SECRETS_ENV, PROXY_DIR_SECRETS_DEFAULT); + signer_envs.insert(k, v); + } + } } - } - volumes.extend(get_log_volume(&cb_config.logs, SIGNER_MODULE_NAME)); + volumes.extend(get_log_volume(&cb_config.logs, SIGNER_MODULE_NAME)); - // networks - let mut signer_networks = vec![SIGNER_NETWORK.to_owned()]; - if metrics_enabled { - signer_networks.push(METRICS_NETWORK.to_owned()); + // networks + let mut signer_networks = vec![SIGNER_NETWORK.to_owned()]; + if metrics_enabled { + signer_networks.push(METRICS_NETWORK.to_owned()); + } + + let signer_service = Service { + container_name: Some("cb_signer".to_owned()), + image: Some(docker_image), + networks: Networks::Simple(signer_networks), + volumes, + environment: Environment::KvPair(signer_envs), + healthcheck: Some(Healthcheck { + test: Some(HealthcheckTest::Single(format!( + "curl -f http://localhost:{signer_port}/status" + ))), + interval: Some("30s".into()), + timeout: Some("5s".into()), + retries: 3, + start_period: Some("5s".into()), + disable: false, + }), + ..Service::default() + }; + + services.insert("cb_signer".to_owned(), Some(signer_service)); } + } + Some(SignerConfig::Dirk { docker_image, cert_path, key_path, ca_cert_path, .. }) => { + if needs_signer_module { + if metrics_enabled { + targets.push(PrometheusTargetConfig { + targets: vec![format!("cb_signer:{metrics_port}")], + labels: PrometheusLabelsConfig { job: "signer".into() }, + }); + } - let signer_service = Service { - container_name: Some("cb_signer".to_owned()), - image: Some(docker_image), - networks: Networks::Simple(signer_networks), - volumes, - environment: Environment::KvPair(signer_envs), - healthcheck: Some(Healthcheck { - test: Some(HealthcheckTest::Single(format!( - "curl -f http://localhost:{signer_port}/status" - ))), - interval: Some("30s".into()), - timeout: Some("5s".into()), - retries: 3, - start_period: Some("5s".into()), - disable: false, - }), - ..Service::default() - }; + let mut signer_envs = IndexMap::from([ + get_env_val(CONFIG_ENV, CONFIG_DEFAULT), + get_env_same(JWTS_ENV), + get_env_uval(SIGNER_PORT_ENV, signer_port as u64), + get_env_val(SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_CERT_DEFAULT), + get_env_val(SIGNER_DIRK_KEY_ENV, SIGNER_DIRK_KEY_DEFAULT), + ]); + + if let Some((key, val)) = chain_spec_env.clone() { + signer_envs.insert(key, val); + } + if metrics_enabled { + let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); + signer_envs.insert(key, val); + } + if log_to_file { + let (key, val) = get_env_val(LOGS_DIR_ENV, LOGS_DIR_DEFAULT); + signer_envs.insert(key, val); + } + + // write jwts to env + envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts)); + + // volumes + let mut volumes = vec![ + config_volume.clone(), + Volumes::Simple(format!( + "{}:{}:ro", + cert_path.display(), + SIGNER_DIRK_CERT_DEFAULT + )), + Volumes::Simple(format!( + "{}:{}:ro", + key_path.display(), + SIGNER_DIRK_KEY_DEFAULT + )), + ]; + volumes.extend(chain_spec_volume.clone()); + volumes.extend(get_log_volume(&cb_config.logs, SIGNER_MODULE_NAME)); + + if let Some(ca_cert_path) = ca_cert_path { + volumes.push(Volumes::Simple(format!( + "{}:{}:ro", + ca_cert_path.display(), + SIGNER_DIRK_CA_CERT_DEFAULT + ))); + let (key, val) = + get_env_val(SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CA_CERT_DEFAULT); + signer_envs.insert(key, val); + } + + // networks + let mut signer_networks = vec![SIGNER_NETWORK.to_owned()]; + if metrics_enabled { + signer_networks.push(METRICS_NETWORK.to_owned()); + } - services.insert("cb_signer".to_owned(), Some(signer_service)); + let signer_service = Service { + container_name: Some("cb_signer".to_owned()), + image: Some(docker_image), + networks: Networks::Simple(signer_networks), + volumes, + environment: Environment::KvPair(signer_envs), + healthcheck: Some(Healthcheck { + test: Some(HealthcheckTest::Single(format!( + "curl -f http://localhost:{signer_port}/status" + ))), + interval: Some("30s".into()), + timeout: Some("5s".into()), + retries: 3, + start_period: Some("5s".into()), + disable: false, + }), + ..Service::default() + }; + + services.insert("cb_signer".to_owned(), Some(signer_service)); + } + } + _ => { + panic!("Signer module required but no signer config provided"); } - } else if cb_config.signer.is_none() && needs_signer_module { - panic!("Signer module required but no signer config provided"); } // setup metrics services diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index 03d990e8..05f85aae 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -47,6 +47,13 @@ pub const SIGNER_DIR_KEYS_DEFAULT: &str = "/keys"; /// Path to `secrets` folder pub const SIGNER_DIR_SECRETS_ENV: &str = "CB_SIGNER_LOADER_SECRETS_DIR"; pub const SIGNER_DIR_SECRETS_DEFAULT: &str = "/secrets"; +/// Path to Dirk certificate +pub const SIGNER_DIRK_CERT_ENV: &str = "CB_SIGNER_DIRK_CERT_FILE"; +pub const SIGNER_DIRK_CERT_DEFAULT: &str = "/certificates/dirk.crt"; +pub const SIGNER_DIRK_KEY_ENV: &str = "CB_SIGNER_DIRK_KEY_FILE"; +pub const SIGNER_DIRK_KEY_DEFAULT: &str = "/certificates/dirk.key"; +pub const SIGNER_DIRK_CA_CERT_ENV: &str = "CB_SIGNER_DIRK_CA_CERT_FILE"; +pub const SIGNER_DIRK_CA_CERT_DEFAULT: &str = "/certificates/ca.crt"; /// Path to store proxies with plaintext keys (testing only) pub const PROXY_DIR_ENV: &str = "CB_PROXY_STORE_DIR"; pub const PROXY_DIR_DEFAULT: &str = "/proxies"; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 60de395e..beb79547 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -13,6 +13,7 @@ use super::{ CommitBoostConfig, SIGNER_PORT_ENV, }; use crate::{ + config::{SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_KEY_ENV}, signer::{ProxyStore, SignerLoader}, types::{Chain, Jwt, ModuleId}, }; @@ -32,14 +33,25 @@ pub enum SignerConfig { }, /// Remote signer module with compatible API like Web3Signer Remote { - /// Complete url of the base API endpoint + /// Complete URL of the base API endpoint url: Url, }, Dirk { + /// Docker image of the module + #[serde(default = "default_signer")] + docker_image: String, + /// Complete URL of a Dirk gateway url: Url, + /// Path to the client certificate cert_path: PathBuf, + /// Path to the client key key_path: PathBuf, - wallet: String, + /// Account to use in format `wallet/account` + account: String, + /// Path to the CA certificate + ca_cert_path: Option, + /// Domain name of the server to use in TLS verification + server_domain: Option, }, } @@ -50,6 +62,7 @@ fn default_signer() -> String { #[derive(Clone, Debug)] pub struct DirkConfig { pub url: Url, + pub account: String, pub client_cert: Identity, pub cert_auth: Option, pub server_domain: Option, @@ -82,24 +95,48 @@ impl StartSignerConfig { store, dirk: None, }), - Some(SignerConfig::Dirk { url, cert_path, key_path, wallet: _ }) => { + Some(SignerConfig::Dirk { + url, + cert_path, + key_path, + account, + ca_cert_path, + server_domain, + .. + }) => { + let cert_path = load_env_var(SIGNER_DIRK_CERT_ENV) + .map(|path| PathBuf::from(path)) + .unwrap_or(cert_path); + let key_path = load_env_var(SIGNER_DIRK_KEY_ENV) + .map(|path| PathBuf::from(path)) + .unwrap_or(key_path); + let ca_cert_path = load_env_var(SIGNER_DIRK_CA_CERT_ENV) + .map(|path| PathBuf::from(path)) + .ok() + .or(ca_cert_path); + Ok(StartSignerConfig { chain: config.chain, - loader: None, server_port, jwts, + loader: None, store: None, dirk: Some(DirkConfig { url, + account, client_cert: Identity::from_pem( std::fs::read_to_string(cert_path)?, std::fs::read_to_string(key_path)?, ), - cert_auth: None, - server_domain: None, + cert_auth: match ca_cert_path { + Some(path) => { + Some(Certificate::from_pem(std::fs::read_to_string(path)?)) + } + None => None, + }, + server_domain, }), }) - // Ok(StartSignerConfig {chain: config.chain, server_port, jwts, dirk}) } Some(SignerConfig::Remote { .. }) => bail!("Remote signer configured"), None => bail!("Signer config is missing"), From 174efb6307cdd6b03514ddcb8527af6d936fd626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 2 Jan 2025 15:53:48 -0300 Subject: [PATCH 004/104] Use already built protobuf rust interfaces --- crates/signer/build.rs | 7 - crates/signer/src/dirk.rs | 58 +- crates/signer/src/lib.rs | 1 + crates/signer/src/manager.rs | 2 +- .../{ => src}/proto/dirk/accountmanager.proto | 0 crates/signer/{ => src}/proto/dirk/dkg.proto | 0 .../{ => src}/proto/dirk/endpoint.proto | 0 crates/signer/{ => src}/proto/dirk/eth2.proto | 0 .../signer/{ => src}/proto/dirk/lister.proto | 0 .../{ => src}/proto/dirk/responsestate.proto | 0 .../signer/{ => src}/proto/dirk/signer.proto | 0 .../{ => src}/proto/dirk/walletmanager.proto | 0 crates/signer/src/proto/google.api.rs | 306 ++++++++ crates/signer/src/proto/mod.rs | 1 + .../third-party/google/api/annotations.proto | 0 .../proto/third-party/google/api/http.proto | 0 crates/signer/src/proto/v1.rs | 714 ++++++++++++++++++ crates/signer/src/service.rs | 16 +- provisioning/pbs.Dockerfile | 1 - provisioning/signer.Dockerfile | 1 - 20 files changed, 1052 insertions(+), 55 deletions(-) delete mode 100644 crates/signer/build.rs rename crates/signer/{ => src}/proto/dirk/accountmanager.proto (100%) rename crates/signer/{ => src}/proto/dirk/dkg.proto (100%) rename crates/signer/{ => src}/proto/dirk/endpoint.proto (100%) rename crates/signer/{ => src}/proto/dirk/eth2.proto (100%) rename crates/signer/{ => src}/proto/dirk/lister.proto (100%) rename crates/signer/{ => src}/proto/dirk/responsestate.proto (100%) rename crates/signer/{ => src}/proto/dirk/signer.proto (100%) rename crates/signer/{ => src}/proto/dirk/walletmanager.proto (100%) create mode 100644 crates/signer/src/proto/google.api.rs create mode 100644 crates/signer/src/proto/mod.rs rename crates/signer/{ => src}/proto/third-party/google/api/annotations.proto (100%) rename crates/signer/{ => src}/proto/third-party/google/api/http.proto (100%) create mode 100644 crates/signer/src/proto/v1.rs diff --git a/crates/signer/build.rs b/crates/signer/build.rs deleted file mode 100644 index c97d3558..00000000 --- a/crates/signer/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -fn main() -> Result<(), Box> { - tonic_build::configure().build_server(false).compile_protos( - &["proto/dirk/lister.proto", "proto/dirk/accountmanager.proto", "proto/dirk/signer.proto"], - &["proto/dirk", "proto/third-party"], - )?; - Ok(()) -} diff --git a/crates/signer/src/dirk.rs b/crates/signer/src/dirk.rs index b458b13f..d33707af 100644 --- a/crates/signer/src/dirk.rs +++ b/crates/signer/src/dirk.rs @@ -1,4 +1,4 @@ -use alloy::{hex, primitives::FixedBytes}; +use alloy::primitives::FixedBytes; use cb_common::{ commit::request::ConsensusProxyMap, config::DirkConfig, @@ -7,19 +7,15 @@ use cb_common::{ }; use tonic::transport::{Channel, ClientTlsConfig}; -use v1::{ - account_manager_client::AccountManagerClient, lister_client::ListerClient, - sign_request::Id as SignerId, signer_client::SignerClient, ListAccountsRequest, ResponseState, - UnlockAccountRequest, +use crate::proto::v1::{ + lister_client::ListerClient, sign_request::Id as SignerId, signer_client::SignerClient, + ListAccountsRequest, ResponseState, }; -pub mod v1 { - tonic::include_proto!("v1"); -} - #[derive(Clone, Debug)] pub struct DirkClient { channel: Channel, + account: String, } impl DirkClient { @@ -35,14 +31,14 @@ impl DirkClient { } let channel = Channel::from_shared(config.url.to_string()) - .map_err(|_| eyre::eyre!("Invalid Dirk URL".to_string()))? + .map_err(|_| eyre::eyre!("Invalid Dirk URL"))? .tls_config(tls_config) - .map_err(|_| eyre::eyre!("Invalid Dirk URL".to_string()))? + .map_err(|_| eyre::eyre!("Invalid Dirk TLS config"))? .connect() .await - .map_err(|e| eyre::eyre!(format!("Couldn't connect to Dirk: {e}")))?; + .map_err(|e| eyre::eyre!("Couldn't connect to Dirk: {e}"))?; - Ok(Self { channel }) + Ok(Self { channel, account: config.account }) } pub async fn get_pubkeys( @@ -50,17 +46,16 @@ impl DirkClient { module_id: ModuleId, ) -> Result, eyre::Error> { let mut client = ListerClient::new(self.channel.clone()); - let dirk_request = tonic::Request::new(ListAccountsRequest { - paths: vec!["wallet1/account1".to_string()], - }); - let dirk_response = client.list_accounts(dirk_request).await.unwrap(); + let pubkeys_request = + tonic::Request::new(ListAccountsRequest { paths: vec![self.account.clone()] }); + let pubkeys_response = client.list_accounts(pubkeys_request).await.unwrap(); - if dirk_response.get_ref().state() != ResponseState::Succeeded { - eyre::bail!("Dirk request failed".to_string()); + if pubkeys_response.get_ref().state() != ResponseState::Succeeded { + eyre::bail!("Get pubkeys request failed".to_string()); } let mut keys = Vec::new(); - for account in dirk_response.into_inner().accounts.iter() { + for account in pubkeys_response.into_inner().accounts.iter() { keys.push(ConsensusProxyMap::new(BlsPublicKey::from(FixedBytes::from_slice( &account.public_key, )))) @@ -70,29 +65,14 @@ impl DirkClient { pub async fn request_signature( &self, - id: SignerId, + domain: [u8; 32], object_root: [u8; 32], ) -> Result { - let mut unlock_client = AccountManagerClient::new(self.channel.clone()); - let unlock_request = tonic::Request::new(UnlockAccountRequest { - account: match &id { - SignerId::Account(account) => account.to_string(), - SignerId::PublicKey(_) => unimplemented!(), - }, - passphrase: hex::decode("736563726574").unwrap(), - }); - - let unlock_response = unlock_client.unlock(unlock_request).await.unwrap(); - if unlock_response.get_ref().state() != ResponseState::Succeeded { - eyre::bail!("Unlock request failed"); - } - let mut signer_client = SignerClient::new(self.channel.clone()); - let sign_request = tonic::Request::new(v1::SignRequest { - id: Some(id), + let sign_request = tonic::Request::new(crate::proto::v1::SignRequest { + id: Some(SignerId::Account(self.account.clone())), + domain: domain.to_vec(), data: object_root.to_vec(), - domain: hex::decode("1ea343e3a29ec9b8de4981dc755220fa10635c6e2ad13a0df6abfc5663c66a88") - .unwrap(), }); let sign_response = signer_client.sign(sign_request).await.unwrap(); diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index df3408c5..b3b7ffee 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -3,4 +3,5 @@ mod dirk; pub mod error; pub mod manager; mod metrics; +pub mod proto; pub mod service; diff --git a/crates/signer/src/manager.rs b/crates/signer/src/manager.rs index 4bf0bfab..69224b23 100644 --- a/crates/signer/src/manager.rs +++ b/crates/signer/src/manager.rs @@ -18,7 +18,7 @@ use tree_hash::TreeHash; use crate::error::SignerModuleError; pub struct SigningManager { - chain: Chain, + pub chain: Chain, proxy_store: Option, consensus_signers: HashMap, proxy_signers: ProxySigners, diff --git a/crates/signer/proto/dirk/accountmanager.proto b/crates/signer/src/proto/dirk/accountmanager.proto similarity index 100% rename from crates/signer/proto/dirk/accountmanager.proto rename to crates/signer/src/proto/dirk/accountmanager.proto diff --git a/crates/signer/proto/dirk/dkg.proto b/crates/signer/src/proto/dirk/dkg.proto similarity index 100% rename from crates/signer/proto/dirk/dkg.proto rename to crates/signer/src/proto/dirk/dkg.proto diff --git a/crates/signer/proto/dirk/endpoint.proto b/crates/signer/src/proto/dirk/endpoint.proto similarity index 100% rename from crates/signer/proto/dirk/endpoint.proto rename to crates/signer/src/proto/dirk/endpoint.proto diff --git a/crates/signer/proto/dirk/eth2.proto b/crates/signer/src/proto/dirk/eth2.proto similarity index 100% rename from crates/signer/proto/dirk/eth2.proto rename to crates/signer/src/proto/dirk/eth2.proto diff --git a/crates/signer/proto/dirk/lister.proto b/crates/signer/src/proto/dirk/lister.proto similarity index 100% rename from crates/signer/proto/dirk/lister.proto rename to crates/signer/src/proto/dirk/lister.proto diff --git a/crates/signer/proto/dirk/responsestate.proto b/crates/signer/src/proto/dirk/responsestate.proto similarity index 100% rename from crates/signer/proto/dirk/responsestate.proto rename to crates/signer/src/proto/dirk/responsestate.proto diff --git a/crates/signer/proto/dirk/signer.proto b/crates/signer/src/proto/dirk/signer.proto similarity index 100% rename from crates/signer/proto/dirk/signer.proto rename to crates/signer/src/proto/dirk/signer.proto diff --git a/crates/signer/proto/dirk/walletmanager.proto b/crates/signer/src/proto/dirk/walletmanager.proto similarity index 100% rename from crates/signer/proto/dirk/walletmanager.proto rename to crates/signer/src/proto/dirk/walletmanager.proto diff --git a/crates/signer/src/proto/google.api.rs b/crates/signer/src/proto/google.api.rs new file mode 100644 index 00000000..243114a3 --- /dev/null +++ b/crates/signer/src/proto/google.api.rs @@ -0,0 +1,306 @@ +// This file is @generated by prost-build. +/// Defines the HTTP configuration for an API service. It contains a list of +/// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +/// to one or more HTTP REST API methods. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Http { + /// A list of HTTP configuration rules that apply to individual API methods. + /// + /// **NOTE:** All service configuration rules follow "last one wins" order. + #[prost(message, repeated, tag = "1")] + pub rules: ::prost::alloc::vec::Vec, + /// When set to true, URL path parmeters will be fully URI-decoded except in + /// cases of single segment matches in reserved expansion, where "%2F" will be + /// left encoded. + /// + /// The default behavior is to not decode RFC 6570 reserved characters in multi + /// segment matches. + #[prost(bool, tag = "2")] + pub fully_decode_reserved_expansion: bool, +} +/// `HttpRule` defines the mapping of an RPC method to one or more HTTP +/// REST API methods. The mapping specifies how different portions of the RPC +/// request message are mapped to URL path, URL query parameters, and +/// HTTP request body. The mapping is typically specified as an +/// `google.api.http` annotation on the RPC method, +/// see "google/api/annotations.proto" for details. +/// +/// The mapping consists of a field specifying the path template and +/// method kind. The path template can refer to fields in the request +/// message, as in the example below which describes a REST GET +/// operation on a resource collection of messages: +/// +/// +/// service Messaging { +/// rpc GetMessage(GetMessageRequest) returns (Message) { +/// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}"; +/// } +/// } +/// message GetMessageRequest { +/// message SubMessage { +/// string subfield = 1; +/// } +/// string message_id = 1; // mapped to the URL +/// SubMessage sub = 2; // `sub.subfield` is url-mapped +/// } +/// message Message { +/// string text = 1; // content of the resource +/// } +/// +/// The same http annotation can alternatively be expressed inside the +/// `GRPC API Configuration` YAML file. +/// +/// http: +/// rules: +/// - selector: .Messaging.GetMessage +/// get: /v1/messages/{message_id}/{sub.subfield} +/// +/// This definition enables an automatic, bidrectional mapping of HTTP +/// JSON to RPC. Example: +/// +/// HTTP | RPC +/// -----|----- +/// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))` +/// +/// In general, not only fields but also field paths can be referenced +/// from a path pattern. Fields mapped to the path pattern cannot be +/// repeated and must have a primitive (non-message) type. +/// +/// Any fields in the request message which are not bound by the path +/// pattern automatically become (optional) HTTP query +/// parameters. Assume the following definition of the request message: +/// +/// +/// service Messaging { +/// rpc GetMessage(GetMessageRequest) returns (Message) { +/// option (google.api.http).get = "/v1/messages/{message_id}"; +/// } +/// } +/// message GetMessageRequest { +/// message SubMessage { +/// string subfield = 1; +/// } +/// string message_id = 1; // mapped to the URL +/// int64 revision = 2; // becomes a parameter +/// SubMessage sub = 3; // `sub.subfield` becomes a parameter +/// } +/// +/// +/// This enables a HTTP JSON to RPC mapping as below: +/// +/// HTTP | RPC +/// -----|----- +/// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))` +/// +/// Note that fields which are mapped to HTTP parameters must have a +/// primitive type or a repeated primitive type. Message types are not +/// allowed. In the case of a repeated type, the parameter can be +/// repeated in the URL, as in `...?param=A¶m=B`. +/// +/// For HTTP method kinds which allow a request body, the `body` field +/// specifies the mapping. Consider a REST update method on the +/// message resource collection: +/// +/// +/// service Messaging { +/// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +/// option (google.api.http) = { +/// put: "/v1/messages/{message_id}" +/// body: "message" +/// }; +/// } +/// } +/// message UpdateMessageRequest { +/// string message_id = 1; // mapped to the URL +/// Message message = 2; // mapped to the body +/// } +/// +/// +/// The following HTTP JSON to RPC mapping is enabled, where the +/// representation of the JSON in the request body is determined by +/// protos JSON encoding: +/// +/// HTTP | RPC +/// -----|----- +/// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })` +/// +/// The special name `*` can be used in the body mapping to define that +/// every field not bound by the path template should be mapped to the +/// request body. This enables the following alternative definition of +/// the update method: +/// +/// service Messaging { +/// rpc UpdateMessage(Message) returns (Message) { +/// option (google.api.http) = { +/// put: "/v1/messages/{message_id}" +/// body: "*" +/// }; +/// } +/// } +/// message Message { +/// string message_id = 1; +/// string text = 2; +/// } +/// +/// +/// The following HTTP JSON to RPC mapping is enabled: +/// +/// HTTP | RPC +/// -----|----- +/// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")` +/// +/// Note that when using `*` in the body mapping, it is not possible to +/// have HTTP parameters, as all fields not bound by the path end in +/// the body. This makes this option more rarely used in practice of +/// defining REST APIs. The common usage of `*` is in custom methods +/// which don't use the URL at all for transferring data. +/// +/// It is possible to define multiple HTTP methods for one RPC by using +/// the `additional_bindings` option. Example: +/// +/// service Messaging { +/// rpc GetMessage(GetMessageRequest) returns (Message) { +/// option (google.api.http) = { +/// get: "/v1/messages/{message_id}" +/// additional_bindings { +/// get: "/v1/users/{user_id}/messages/{message_id}" +/// } +/// }; +/// } +/// } +/// message GetMessageRequest { +/// string message_id = 1; +/// string user_id = 2; +/// } +/// +/// +/// This enables the following two alternative HTTP JSON to RPC +/// mappings: +/// +/// HTTP | RPC +/// -----|----- +/// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +/// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")` +/// +/// # Rules for HTTP mapping +/// +/// The rules for mapping HTTP path, query parameters, and body fields +/// to the request message are as follows: +/// +/// 1. The `body` field specifies either `*` or a field path, or is +/// omitted. If omitted, it indicates there is no HTTP request body. +/// 2. Leaf fields (recursive expansion of nested messages in the +/// request) can be classified into three types: +/// (a) Matched in the URL template. +/// (b) Covered by body (if body is `*`, everything except (a) fields; +/// else everything under the body field) +/// (c) All other fields. +/// 3. URL query parameters found in the HTTP request are mapped to (c) fields. +/// 4. Any body sent with an HTTP request can contain only (b) fields. +/// +/// The syntax of the path template is as follows: +/// +/// Template = "/" Segments \[ Verb \] ; +/// Segments = Segment { "/" Segment } ; +/// Segment = "*" | "**" | LITERAL | Variable ; +/// Variable = "{" FieldPath \[ "=" Segments \] "}" ; +/// FieldPath = IDENT { "." IDENT } ; +/// Verb = ":" LITERAL ; +/// +/// The syntax `*` matches a single path segment. The syntax `**` matches zero +/// or more path segments, which must be the last part of the path except the +/// `Verb`. The syntax `LITERAL` matches literal text in the path. +/// +/// The syntax `Variable` matches part of the URL path as specified by its +/// template. A variable template must not contain other variables. If a variable +/// matches a single path segment, its template may be omitted, e.g. `{var}` +/// is equivalent to `{var=*}`. +/// +/// If a variable contains exactly one path segment, such as `"{var}"` or +/// `"{var=*}"`, when such a variable is expanded into a URL path, all characters +/// except `\[-_.~0-9a-zA-Z\]` are percent-encoded. Such variables show up in the +/// Discovery Document as `{var}`. +/// +/// If a variable contains one or more path segments, such as `"{var=foo/*}"` +/// or `"{var=**}"`, when such a variable is expanded into a URL path, all +/// characters except `\[-_.~/0-9a-zA-Z\]` are percent-encoded. Such variables +/// show up in the Discovery Document as `{+var}`. +/// +/// NOTE: While the single segment variable matches the semantics of +/// [RFC 6570]() Section 3.2.2 +/// Simple String Expansion, the multi segment variable **does not** match +/// RFC 6570 Reserved Expansion. The reason is that the Reserved Expansion +/// does not expand special characters like `?` and `#`, which would lead +/// to invalid URLs. +/// +/// NOTE: the field paths in variables and in the `body` must not refer to +/// repeated fields or map fields. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct HttpRule { + /// Selects methods to which this rule applies. + /// + /// Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + #[prost(string, tag = "1")] + pub selector: ::prost::alloc::string::String, + /// The name of the request field whose value is mapped to the HTTP body, or + /// `*` for mapping all fields not captured by the path pattern to the HTTP + /// body. NOTE: the referred field must not be a repeated field and must be + /// present at the top-level of request message type. + #[prost(string, tag = "7")] + pub body: ::prost::alloc::string::String, + /// Optional. The name of the response field whose value is mapped to the HTTP + /// body of response. Other response fields are ignored. When + /// not set, the response message will be used as HTTP body of response. + #[prost(string, tag = "12")] + pub response_body: ::prost::alloc::string::String, + /// Additional HTTP bindings for the selector. Nested bindings must + /// not contain an `additional_bindings` field themselves (that is, + /// the nesting may only be one level deep). + #[prost(message, repeated, tag = "11")] + pub additional_bindings: ::prost::alloc::vec::Vec, + /// Determines the URL pattern is matched by this rules. This pattern can be + /// used with any of the {get|put|post|delete|patch} methods. A custom method + /// can be defined using the 'custom' field. + #[prost(oneof = "http_rule::Pattern", tags = "2, 3, 4, 5, 6, 8")] + pub pattern: ::core::option::Option, +} +/// Nested message and enum types in `HttpRule`. +pub mod http_rule { + /// Determines the URL pattern is matched by this rules. This pattern can be + /// used with any of the {get|put|post|delete|patch} methods. A custom method + /// can be defined using the 'custom' field. + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Pattern { + /// Used for listing and getting information about resources. + #[prost(string, tag = "2")] + Get(::prost::alloc::string::String), + /// Used for updating a resource. + #[prost(string, tag = "3")] + Put(::prost::alloc::string::String), + /// Used for creating a resource. + #[prost(string, tag = "4")] + Post(::prost::alloc::string::String), + /// Used for deleting a resource. + #[prost(string, tag = "5")] + Delete(::prost::alloc::string::String), + /// Used for updating a resource. + #[prost(string, tag = "6")] + Patch(::prost::alloc::string::String), + /// The custom pattern is used for specifying an HTTP method that is not + /// included in the `pattern` field, such as HEAD, or "*" to leave the + /// HTTP method unspecified for this rule. The wild-card rule is useful + /// for services that provide content to Web (HTML) clients. + #[prost(message, tag = "8")] + Custom(super::CustomHttpPattern), + } +} +/// A custom pattern is used for defining custom HTTP verb. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CustomHttpPattern { + /// The name of this custom HTTP verb. + #[prost(string, tag = "1")] + pub kind: ::prost::alloc::string::String, + /// The path matched by this custom verb. + #[prost(string, tag = "2")] + pub path: ::prost::alloc::string::String, +} diff --git a/crates/signer/src/proto/mod.rs b/crates/signer/src/proto/mod.rs new file mode 100644 index 00000000..a3a6d96c --- /dev/null +++ b/crates/signer/src/proto/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/crates/signer/proto/third-party/google/api/annotations.proto b/crates/signer/src/proto/third-party/google/api/annotations.proto similarity index 100% rename from crates/signer/proto/third-party/google/api/annotations.proto rename to crates/signer/src/proto/third-party/google/api/annotations.proto diff --git a/crates/signer/proto/third-party/google/api/http.proto b/crates/signer/src/proto/third-party/google/api/http.proto similarity index 100% rename from crates/signer/proto/third-party/google/api/http.proto rename to crates/signer/src/proto/third-party/google/api/http.proto diff --git a/crates/signer/src/proto/v1.rs b/crates/signer/src/proto/v1.rs new file mode 100644 index 00000000..36984aa0 --- /dev/null +++ b/crates/signer/src/proto/v1.rs @@ -0,0 +1,714 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Endpoint { + #[prost(uint64, tag = "1")] + pub id: u64, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, + #[prost(uint32, tag = "3")] + pub port: u32, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ResponseState { + /// UNKNOWN occurs when no information about the response is available. + Unknown = 0, + /// SUCCEEDED occurs when a request was successful. + Succeeded = 1, + /// DENIED occurs when a request was denied. + Denied = 2, + /// FAILED occurs when a request failed to complete. + Failed = 3, +} +impl ResponseState { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unknown => "UNKNOWN", + Self::Succeeded => "SUCCEEDED", + Self::Denied => "DENIED", + Self::Failed => "FAILED", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNKNOWN" => Some(Self::Unknown), + "SUCCEEDED" => Some(Self::Succeeded), + "DENIED" => Some(Self::Denied), + "FAILED" => Some(Self::Failed), + _ => None, + } + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListAccountsRequest { + #[prost(string, repeated, tag = "1")] + pub paths: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListAccountsResponse { + #[prost(enumeration = "ResponseState", tag = "1")] + pub state: i32, + #[prost(message, repeated, tag = "2")] + pub accounts: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "3")] + pub distributed_accounts: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Account { + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + #[prost(bytes = "vec", tag = "2")] + pub public_key: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "3")] + pub uuid: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DistributedAccount { + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + #[prost(bytes = "vec", tag = "2")] + pub public_key: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "3")] + pub participants: ::prost::alloc::vec::Vec, + #[prost(uint32, tag = "4")] + pub signing_threshold: u32, + #[prost(bytes = "vec", tag = "5")] + pub uuid: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "6")] + pub composite_public_key: ::prost::alloc::vec::Vec, +} +/// Generated client implementations. +pub mod lister_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct ListerClient { + inner: tonic::client::Grpc, + } + impl ListerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ListerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ListerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ListerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn list_accounts( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/v1.Lister/ListAccounts"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("v1.Lister", "ListAccounts")); + self.inner.unary(req, path, codec).await + } + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UnlockAccountRequest { + #[prost(string, tag = "1")] + pub account: ::prost::alloc::string::String, + #[prost(bytes = "vec", tag = "2")] + pub passphrase: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LockAccountRequest { + #[prost(string, tag = "1")] + pub account: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct UnlockAccountResponse { + #[prost(enumeration = "ResponseState", tag = "1")] + pub state: i32, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct LockAccountResponse { + #[prost(enumeration = "ResponseState", tag = "1")] + pub state: i32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateRequest { + #[prost(string, tag = "1")] + pub account: ::prost::alloc::string::String, + #[prost(bytes = "vec", tag = "2")] + pub passphrase: ::prost::alloc::vec::Vec, + #[prost(uint32, tag = "3")] + pub participants: u32, + #[prost(uint32, tag = "4")] + pub signing_threshold: u32, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenerateResponse { + #[prost(enumeration = "ResponseState", tag = "1")] + pub state: i32, + #[prost(string, tag = "2")] + pub message: ::prost::alloc::string::String, + #[prost(bytes = "vec", tag = "3")] + pub public_key: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "4")] + pub participants: ::prost::alloc::vec::Vec, +} +/// Generated client implementations. +pub mod account_manager_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct AccountManagerClient { + inner: tonic::client::Grpc, + } + impl AccountManagerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl AccountManagerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> AccountManagerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + AccountManagerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn unlock( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/v1.AccountManager/Unlock"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("v1.AccountManager", "Unlock")); + self.inner.unary(req, path, codec).await + } + pub async fn lock( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/v1.AccountManager/Lock"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("v1.AccountManager", "Lock")); + self.inner.unary(req, path, codec).await + } + pub async fn generate( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/v1.AccountManager/Generate", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("v1.AccountManager", "Generate")); + self.inner.unary(req, path, codec).await + } + } +} +/// AttestationData is defined at +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AttestationData { + #[prost(uint64, tag = "1")] + pub slot: u64, + #[prost(uint64, tag = "2")] + pub committee_index: u64, + #[prost(bytes = "vec", tag = "3")] + pub beacon_block_root: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "4")] + pub source: ::core::option::Option, + #[prost(message, optional, tag = "5")] + pub target: ::core::option::Option, +} +/// Checkpoint is defined at +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Checkpoint { + #[prost(uint64, tag = "1")] + pub epoch: u64, + #[prost(bytes = "vec", tag = "2")] + pub root: ::prost::alloc::vec::Vec, +} +/// BeaconBlockheader is defined at +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BeaconBlockHeader { + #[prost(uint64, tag = "1")] + pub slot: u64, + #[prost(uint64, tag = "2")] + pub proposer_index: u64, + #[prost(bytes = "vec", tag = "3")] + pub parent_root: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "4")] + pub state_root: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "5")] + pub body_root: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignRequest { + #[prost(bytes = "vec", tag = "3")] + pub data: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "4")] + pub domain: ::prost::alloc::vec::Vec, + #[prost(oneof = "sign_request::Id", tags = "1, 2")] + pub id: ::core::option::Option, +} +/// Nested message and enum types in `SignRequest`. +pub mod sign_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Id { + #[prost(bytes, tag = "1")] + PublicKey(::prost::alloc::vec::Vec), + #[prost(string, tag = "2")] + Account(::prost::alloc::string::String), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MultisignRequest { + #[prost(message, repeated, tag = "1")] + pub requests: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignBeaconAttestationRequest { + #[prost(bytes = "vec", tag = "3")] + pub domain: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "4")] + pub data: ::core::option::Option, + #[prost(oneof = "sign_beacon_attestation_request::Id", tags = "1, 2")] + pub id: ::core::option::Option, +} +/// Nested message and enum types in `SignBeaconAttestationRequest`. +pub mod sign_beacon_attestation_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Id { + #[prost(bytes, tag = "1")] + PublicKey(::prost::alloc::vec::Vec), + #[prost(string, tag = "2")] + Account(::prost::alloc::string::String), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignBeaconAttestationsRequest { + #[prost(message, repeated, tag = "1")] + pub requests: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignBeaconProposalRequest { + #[prost(bytes = "vec", tag = "3")] + pub domain: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "4")] + pub data: ::core::option::Option, + #[prost(oneof = "sign_beacon_proposal_request::Id", tags = "1, 2")] + pub id: ::core::option::Option, +} +/// Nested message and enum types in `SignBeaconProposalRequest`. +pub mod sign_beacon_proposal_request { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Id { + #[prost(bytes, tag = "1")] + PublicKey(::prost::alloc::vec::Vec), + #[prost(string, tag = "2")] + Account(::prost::alloc::string::String), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SignResponse { + #[prost(enumeration = "ResponseState", tag = "1")] + pub state: i32, + #[prost(bytes = "vec", tag = "2")] + pub signature: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MultisignResponse { + #[prost(message, repeated, tag = "1")] + pub responses: ::prost::alloc::vec::Vec, +} +/// Generated client implementations. +pub mod signer_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct SignerClient { + inner: tonic::client::Grpc, + } + impl SignerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl SignerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> SignerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + SignerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn sign( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/v1.Signer/Sign"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("v1.Signer", "Sign")); + self.inner.unary(req, path, codec).await + } + pub async fn multisign( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/v1.Signer/Multisign"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("v1.Signer", "Multisign")); + self.inner.unary(req, path, codec).await + } + pub async fn sign_beacon_attestation( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/v1.Signer/SignBeaconAttestation", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("v1.Signer", "SignBeaconAttestation")); + self.inner.unary(req, path, codec).await + } + pub async fn sign_beacon_attestations( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/v1.Signer/SignBeaconAttestations", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("v1.Signer", "SignBeaconAttestations")); + self.inner.unary(req, path, codec).await + } + pub async fn sign_beacon_proposal( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/v1.Signer/SignBeaconProposal", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("v1.Signer", "SignBeaconProposal")); + self.inner.unary(req, path, codec).await + } + } +} diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 054f1097..9338f219 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -21,7 +21,8 @@ use cb_common::{ }, }, config::StartSignerConfig, - constants::COMMIT_BOOST_VERSION, + constants::{COMMIT_BOOST_DOMAIN, COMMIT_BOOST_VERSION}, + signature::compute_domain, types::{Jwt, ModuleId}, }; use cb_metrics::provider::MetricsProvider; @@ -32,7 +33,7 @@ use tracing::{debug, error, info, warn}; use uuid::Uuid; use crate::{ - dirk::{v1::sign_request::Id as SignerId, DirkClient}, + dirk::DirkClient, error::SignerModuleError, manager::SigningManager, metrics::{uri_to_tag, SIGNER_METRICS_REGISTRY, SIGNER_STATUS}, @@ -186,10 +187,13 @@ async fn handle_request_signature( SignRequest::ProxyEcdsa(SignProxyRequest { object_root, .. }) => object_root, }; let signature_response = if let Some(dirk) = state.dirk { - dirk.request_signature(SignerId::Account("wallet1/account1".to_string()), object_root) - .await - .map(|sig| Json(sig).into_response()) - .map_err(|e| SignerModuleError::Internal(e.to_string())) + dirk.request_signature( + compute_domain(signing_manager.chain, COMMIT_BOOST_DOMAIN), + object_root, + ) + .await + .map(|sig| Json(sig).into_response()) + .map_err(|e| SignerModuleError::Internal(e.to_string())) } else { match request { SignRequest::Consensus(SignConsensusRequest { pubkey, object_root }) => signing_manager diff --git a/provisioning/pbs.Dockerfile b/provisioning/pbs.Dockerfile index cf142622..9d2a2fcf 100644 --- a/provisioning/pbs.Dockerfile +++ b/provisioning/pbs.Dockerfile @@ -8,7 +8,6 @@ RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json -RUN apt-get update && apt-get install -y protobuf-compiler RUN cargo chef cook --release --recipe-path recipe.json COPY . . diff --git a/provisioning/signer.Dockerfile b/provisioning/signer.Dockerfile index 9872e570..3f267806 100644 --- a/provisioning/signer.Dockerfile +++ b/provisioning/signer.Dockerfile @@ -8,7 +8,6 @@ RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json -RUN apt-get update && apt-get install -y protobuf-compiler RUN cargo chef cook --release --recipe-path recipe.json COPY . . From 53257c6e139a738d30814c9fa7d40313e1bbdbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 2 Jan 2025 15:54:16 -0300 Subject: [PATCH 005/104] Throw error on generate_proxy_key if using Dirk --- crates/signer/src/error.rs | 4 ++++ crates/signer/src/service.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index 820bdeed..39f59cd1 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -16,6 +16,9 @@ pub enum SignerModuleError { #[error("unknown proxy signer: 0x{}", hex::encode(.0))] UnknownProxySigner(Vec), + #[error("Dirk signer does not support this operation")] + DirkNotSupported, + #[error("internal error {0}")] Internal(String), } @@ -26,6 +29,7 @@ impl IntoResponse for SignerModuleError { SignerModuleError::Unauthorized => StatusCode::UNAUTHORIZED, SignerModuleError::UnknownConsensusSigner(_) => StatusCode::NOT_FOUND, SignerModuleError::UnknownProxySigner(_) => StatusCode::NOT_FOUND, + SignerModuleError::DirkNotSupported => StatusCode::BAD_REQUEST, SignerModuleError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, }; diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 9338f219..4531633a 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -235,6 +235,10 @@ async fn handle_generate_proxy( debug!(event = "generate_proxy", module_id=?module_id, ?req_id, "New request"); + if state.dirk.is_some() { + return Err(SignerModuleError::DirkNotSupported); + } + let mut signing_manager = state.manager.write().await; let response = match request.scheme { From 1d59394820b30aa924c131073b6b6d7c18273ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 2 Jan 2025 15:54:55 -0300 Subject: [PATCH 006/104] Update DA_COMMIT example to be able to test Dirk signer --- examples/da_commit/src/main.rs | 56 ++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index 88767f00..661b9c93 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -33,6 +33,12 @@ struct DaCommitService { #[derive(Debug, Deserialize)] struct ExtraConfig { sleep_secs: u64, + #[serde(default = "default_delegation")] + use_delegation_keys: bool, +} + +fn default_delegation() -> bool { + true } impl DaCommitService { @@ -45,25 +51,36 @@ impl DaCommitService { let pubkey = pubkeys.first().ok_or_eyre("no key available")?.consensus; info!("Registered validator {pubkey}"); - let proxy_delegation_bls = self.config.signer_client.generate_proxy_key_bls(pubkey).await?; - info!("Obtained a BLS proxy delegation:\n{proxy_delegation_bls}"); - let proxy_bls = proxy_delegation_bls.message.proxy; + if self.config.extra.use_delegation_keys { + let proxy_delegation_bls = + self.config.signer_client.generate_proxy_key_bls(pubkey).await?; + info!("Obtained a BLS proxy delegation:\n{proxy_delegation_bls}"); + let proxy_bls = proxy_delegation_bls.message.proxy; - let proxy_delegation_ecdsa = - self.config.signer_client.generate_proxy_key_ecdsa(pubkey).await?; - info!("Obtained an ECDSA proxy delegation:\n{proxy_delegation_ecdsa}"); - let proxy_ecdsa = proxy_delegation_ecdsa.message.proxy; + let proxy_delegation_ecdsa = + self.config.signer_client.generate_proxy_key_ecdsa(pubkey).await?; + info!("Obtained an ECDSA proxy delegation:\n{proxy_delegation_ecdsa}"); + let proxy_ecdsa = proxy_delegation_ecdsa.message.proxy; - let mut data = 0; + let mut data = 0; + + loop { + self.send_multiple_requests(data, pubkey, proxy_bls, proxy_ecdsa).await?; + sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; + data += 1; + } + } else { + let mut data = 0; - loop { - self.send_request(data, pubkey, proxy_bls, proxy_ecdsa).await?; - sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; - data += 1; + loop { + self.send_request(data, pubkey).await?; + sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; + data += 1; + } } } - pub async fn send_request( + pub async fn send_multiple_requests( &self, data: u64, pubkey: BlsPublicKey, @@ -96,6 +113,19 @@ impl DaCommitService { Ok(()) } + + pub async fn send_request(&self, data: u64, pubkey: BlsPublicKey) -> Result<()> { + let datagram = Datagram { data }; + + let request = SignConsensusRequest::builder(pubkey).with_msg(&datagram); + let signature = self.config.signer_client.request_consensus_signature(request).await?; + + info!("Proposer commitment (consensus): {}", signature); + + SIG_RECEIVED_COUNTER.inc(); + + Ok(()) + } } #[tokio::main] From 78c857a28c9f955cb7db804ca3e185dd69897020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 2 Jan 2025 16:04:40 -0300 Subject: [PATCH 007/104] Refactor signer handlers --- crates/signer/src/service.rs | 52 ++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 4531633a..36d6282e 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -155,13 +155,17 @@ async fn handle_get_pubkeys( debug!(event = "get_pubkeys", ?req_id, "New request"); - let keys = if let Some(dirk) = state.dirk { - dirk.get_pubkeys(module_id).await.map_err(|e| SignerModuleError::Internal(e.to_string()))? - } else { - let signing_manager = state.manager.read().await; - signing_manager - .get_consensus_proxy_maps(&module_id) - .map_err(|err| SignerModuleError::Internal(err.to_string()))? + let keys = match state.dirk { + Some(dirk) => dirk + .get_pubkeys(module_id) + .await + .map_err(|e| SignerModuleError::Internal(e.to_string()))?, + None => { + let signing_manager = state.manager.read().await; + signing_manager + .get_consensus_proxy_maps(&module_id) + .map_err(|err| SignerModuleError::Internal(err.to_string()))? + } }; let res = GetPubkeysResponse { keys }; @@ -181,21 +185,22 @@ async fn handle_request_signature( let signing_manager = state.manager.read().await; - let object_root = match request { - SignRequest::Consensus(SignConsensusRequest { object_root, .. }) => object_root, - SignRequest::ProxyBls(SignProxyRequest { object_root, .. }) => object_root, - SignRequest::ProxyEcdsa(SignProxyRequest { object_root, .. }) => object_root, - }; - let signature_response = if let Some(dirk) = state.dirk { - dirk.request_signature( - compute_domain(signing_manager.chain, COMMIT_BOOST_DOMAIN), - object_root, - ) - .await - .map(|sig| Json(sig).into_response()) - .map_err(|e| SignerModuleError::Internal(e.to_string())) - } else { - match request { + let signature_response = match state.dirk { + Some(dirk) => match request { + SignRequest::Consensus(SignConsensusRequest { object_root, .. }) => dirk + .request_signature( + compute_domain(signing_manager.chain, COMMIT_BOOST_DOMAIN), + object_root, + ) + .await + .map(|sig| Json(sig).into_response()) + .map_err(|e| SignerModuleError::Internal(e.to_string())), + _ => { + error!("Proxy sign request not supported with Dirk"); + Err(SignerModuleError::DirkNotSupported) + } + }, + None => match request { SignRequest::Consensus(SignConsensusRequest { pubkey, object_root }) => signing_manager .sign_consensus(&pubkey, &object_root) .await @@ -220,7 +225,7 @@ async fn handle_request_signature( .await .map(|sig| Json(sig).into_response()) } - } + }, }; Ok(signature_response?) @@ -236,6 +241,7 @@ async fn handle_generate_proxy( debug!(event = "generate_proxy", module_id=?module_id, ?req_id, "New request"); if state.dirk.is_some() { + error!("Generate proxy not supported with Dirk"); return Err(SignerModuleError::DirkNotSupported); } From 755c2fd7e42a4b04bb05ddd34a2136365e081aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 3 Jan 2025 15:29:51 -0300 Subject: [PATCH 008/104] Add BLS proxies --- crates/common/src/config/signer.rs | 10 +-- crates/signer/src/dirk.rs | 136 +++++++++++++++++++++++++---- crates/signer/src/service.rs | 63 ++++++++----- 3 files changed, 165 insertions(+), 44 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index beb79547..31caf0de 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -46,8 +46,8 @@ pub enum SignerConfig { cert_path: PathBuf, /// Path to the client key key_path: PathBuf, - /// Account to use in format `wallet/account` - account: String, + /// Wallet to use + wallet: String, /// Path to the CA certificate ca_cert_path: Option, /// Domain name of the server to use in TLS verification @@ -62,7 +62,7 @@ fn default_signer() -> String { #[derive(Clone, Debug)] pub struct DirkConfig { pub url: Url, - pub account: String, + pub wallet: String, pub client_cert: Identity, pub cert_auth: Option, pub server_domain: Option, @@ -99,7 +99,7 @@ impl StartSignerConfig { url, cert_path, key_path, - account, + wallet, ca_cert_path, server_domain, .. @@ -123,7 +123,7 @@ impl StartSignerConfig { store: None, dirk: Some(DirkConfig { url, - account, + wallet, client_cert: Identity::from_pem( std::fs::read_to_string(cert_path)?, std::fs::read_to_string(key_path)?, diff --git a/crates/signer/src/dirk.rs b/crates/signer/src/dirk.rs index d33707af..40191ce1 100644 --- a/crates/signer/src/dirk.rs +++ b/crates/signer/src/dirk.rs @@ -1,6 +1,6 @@ use alloy::primitives::FixedBytes; use cb_common::{ - commit::request::ConsensusProxyMap, + commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, config::DirkConfig, signer::{BlsPublicKey, BlsSignature}, types::ModuleId, @@ -8,14 +8,15 @@ use cb_common::{ use tonic::transport::{Channel, ClientTlsConfig}; use crate::proto::v1::{ - lister_client::ListerClient, sign_request::Id as SignerId, signer_client::SignerClient, - ListAccountsRequest, ResponseState, + account_manager_client::AccountManagerClient, lister_client::ListerClient, + sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, + GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, UnlockAccountRequest, }; #[derive(Clone, Debug)] pub struct DirkClient { channel: Channel, - account: String, + wallet: String, } impl DirkClient { @@ -38,39 +39,114 @@ impl DirkClient { .await .map_err(|e| eyre::eyre!("Couldn't connect to Dirk: {e}"))?; - Ok(Self { channel, account: config.account }) + Ok(Self { channel, wallet: config.wallet }) } - pub async fn get_pubkeys( - &self, - module_id: ModuleId, - ) -> Result, eyre::Error> { + async fn list_accounts(&self) -> eyre::Result> { let mut client = ListerClient::new(self.channel.clone()); let pubkeys_request = - tonic::Request::new(ListAccountsRequest { paths: vec![self.account.clone()] }); + tonic::Request::new(ListAccountsRequest { paths: vec![self.wallet.clone()] }); let pubkeys_response = client.list_accounts(pubkeys_request).await.unwrap(); if pubkeys_response.get_ref().state() != ResponseState::Succeeded { eyre::bail!("Get pubkeys request failed".to_string()); } - let mut keys = Vec::new(); - for account in pubkeys_response.into_inner().accounts.iter() { - keys.push(ConsensusProxyMap::new(BlsPublicKey::from(FixedBytes::from_slice( - &account.public_key, - )))) - } + Ok(pubkeys_response.into_inner().accounts) + } + + pub async fn get_pubkeys(&self) -> eyre::Result> { + let accounts = self.list_accounts().await?; + + let keys = accounts + .iter() + .filter_map(|account| { + if account.name == format!("{}/consensus", self.wallet.clone()) { + Some(ConsensusProxyMap::new(BlsPublicKey::from(FixedBytes::from_slice( + &account.public_key, + )))) + } else { + None + } + }) + .collect(); Ok(keys) } - pub async fn request_signature( + pub async fn get_consensus_proxy_maps( + &self, + module_id: &ModuleId, + ) -> eyre::Result> { + let accounts = self.list_accounts().await?; + + let consensus_pubkey = accounts + .iter() + .find(|account| account.name == format!("{}/consensus", self.wallet.clone())) + .map(|account| BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) + .ok_or_else(|| eyre::eyre!("No consensus key found"))?; + + let proxy_keys = accounts + .iter() + .filter(|account| account.name.starts_with(&format!("{}/{module_id}/", self.wallet))) + .map(|account| BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) + .collect::>(); + + Ok(vec![ConsensusProxyMap { + consensus: consensus_pubkey, + proxy_bls: proxy_keys, + proxy_ecdsa: vec![], + }]) + } + + pub async fn generate_proxy_key( + &self, + module_id: ModuleId, + consensus_pubkey: BlsPublicKey, + ) -> Result, eyre::Error> { + let uuid = uuid::Uuid::new_v4(); + + let mut client = AccountManagerClient::new(self.channel.clone()); + let generate_request = tonic::Request::new(GenerateRequest { + account: format!("{}/{module_id}/{uuid}", self.wallet), + passphrase: vec![0x73, 0x65, 0x63, 0x72, 0x65, 0x74], // "secret" + participants: 1, + signing_threshold: 1, + }); + + let generate_response = client.generate(generate_request).await?; + if generate_response.get_ref().state() != ResponseState::Succeeded { + eyre::bail!("Generate request failed"); + } + + let proxy_key = + BlsPublicKey::from(FixedBytes::from_slice(&generate_response.into_inner().public_key)); + + let mut unlock_client = AccountManagerClient::new(self.channel.clone()); + let unlock_request = tonic::Request::new(UnlockAccountRequest { + account: format!("{}/{module_id}/{uuid}", self.wallet), + passphrase: vec![0x73, 0x65, 0x63, 0x72, 0x65, 0x74], // "secret" + }); + + let unlock_response = unlock_client.unlock(unlock_request).await?; + if unlock_response.get_ref().state() != ResponseState::Succeeded { + eyre::bail!("Unlock request failed"); + } + + Ok(SignedProxyDelegation { + message: ProxyDelegation { delegator: consensus_pubkey, proxy: proxy_key }, + signature: BlsSignature::random(), + }) + } + + async fn request_signature( &self, + signer_id: SignerId, domain: [u8; 32], object_root: [u8; 32], ) -> Result { let mut signer_client = SignerClient::new(self.channel.clone()); - let sign_request = tonic::Request::new(crate::proto::v1::SignRequest { - id: Some(SignerId::Account(self.account.clone())), + let sign_request = tonic::Request::new(SignRequest { + id: Some(signer_id), domain: domain.to_vec(), data: object_root.to_vec(), }); @@ -82,4 +158,26 @@ impl DirkClient { Ok(BlsSignature::from(FixedBytes::from_slice(&sign_response.into_inner().signature))) } + + pub async fn request_consensus_signature( + &self, + domain: [u8; 32], + object_root: [u8; 32], + ) -> eyre::Result { + self.request_signature( + SignerId::Account(format!("{}/consensus", self.wallet.clone())), + domain, + object_root, + ) + .await + } + + pub async fn request_proxy_bls_signature( + &self, + bls_key: &BlsPublicKey, + domain: [u8; 32], + object_root: [u8; 32], + ) -> eyre::Result { + self.request_signature(SignerId::PublicKey(bls_key.0.to_vec()), domain, object_root).await + } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 36d6282e..060ed9d7 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -157,7 +157,7 @@ async fn handle_get_pubkeys( let keys = match state.dirk { Some(dirk) => dirk - .get_pubkeys(module_id) + .get_consensus_proxy_maps(&module_id) .await .map_err(|e| SignerModuleError::Internal(e.to_string()))?, None => { @@ -188,7 +188,16 @@ async fn handle_request_signature( let signature_response = match state.dirk { Some(dirk) => match request { SignRequest::Consensus(SignConsensusRequest { object_root, .. }) => dirk - .request_signature( + .request_consensus_signature( + compute_domain(signing_manager.chain, COMMIT_BOOST_DOMAIN), + object_root, + ) + .await + .map(|sig| Json(sig).into_response()) + .map_err(|e| SignerModuleError::Internal(e.to_string())), + SignRequest::ProxyBls(SignProxyRequest { pubkey: bls_pk, object_root }) => dirk + .request_proxy_bls_signature( + &bls_pk, compute_domain(signing_manager.chain, COMMIT_BOOST_DOMAIN), object_root, ) @@ -196,7 +205,7 @@ async fn handle_request_signature( .map(|sig| Json(sig).into_response()) .map_err(|e| SignerModuleError::Internal(e.to_string())), _ => { - error!("Proxy sign request not supported with Dirk"); + error!("ECDSA proxy sign request not supported with Dirk"); Err(SignerModuleError::DirkNotSupported) } }, @@ -240,23 +249,37 @@ async fn handle_generate_proxy( debug!(event = "generate_proxy", module_id=?module_id, ?req_id, "New request"); - if state.dirk.is_some() { - error!("Generate proxy not supported with Dirk"); - return Err(SignerModuleError::DirkNotSupported); - } - - let mut signing_manager = state.manager.write().await; - - let response = match request.scheme { - EncryptionScheme::Bls => { - let proxy_delegation = - signing_manager.create_proxy_bls(module_id, request.consensus_pubkey).await?; - Json(proxy_delegation).into_response() - } - EncryptionScheme::Ecdsa => { - let proxy_delegation = - signing_manager.create_proxy_ecdsa(module_id, request.consensus_pubkey).await?; - Json(proxy_delegation).into_response() + let response = match state.dirk { + Some(dirk) => match request.scheme { + EncryptionScheme::Bls => { + let proxy_delegation = dirk + .generate_proxy_key(module_id, request.consensus_pubkey) + .await + .map_err(|e| SignerModuleError::Internal(e.to_string()))?; + Json(proxy_delegation).into_response() + } + EncryptionScheme::Ecdsa => { + error!("ECDSA proxy generation not supported with Dirk"); + return Err(SignerModuleError::DirkNotSupported); + } + }, + None => { + let mut signing_manager = state.manager.write().await; + + match request.scheme { + EncryptionScheme::Bls => { + let proxy_delegation = signing_manager + .create_proxy_bls(module_id, request.consensus_pubkey) + .await?; + Json(proxy_delegation).into_response() + } + EncryptionScheme::Ecdsa => { + let proxy_delegation = signing_manager + .create_proxy_ecdsa(module_id, request.consensus_pubkey) + .await?; + Json(proxy_delegation).into_response() + } + } } }; From f015d39081d6ff7bdd8037e97f1fba7747ac2308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 3 Jan 2025 17:07:40 -0300 Subject: [PATCH 009/104] Refactor SigningState --- crates/common/src/signer/types.rs | 2 +- crates/signer/src/dirk.rs | 66 ++++---- crates/signer/src/manager.rs | 9 +- crates/signer/src/service.rs | 256 +++++++++++++++++------------- 4 files changed, 179 insertions(+), 154 deletions(-) diff --git a/crates/common/src/signer/types.rs b/crates/common/src/signer/types.rs index 4071f858..fdb2c11f 100644 --- a/crates/common/src/signer/types.rs +++ b/crates/common/src/signer/types.rs @@ -34,7 +34,7 @@ pub struct EcdsaProxySigner { pub delegation: SignedProxyDelegationEcdsa, } -#[derive(Default)] +#[derive(Clone, Default)] pub struct ProxySigners { pub bls_signers: HashMap, pub ecdsa_signers: HashMap, diff --git a/crates/signer/src/dirk.rs b/crates/signer/src/dirk.rs index 40191ce1..8fab2f33 100644 --- a/crates/signer/src/dirk.rs +++ b/crates/signer/src/dirk.rs @@ -2,8 +2,10 @@ use alloy::primitives::FixedBytes; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, config::DirkConfig, + constants::COMMIT_BOOST_DOMAIN, + signature::compute_domain, signer::{BlsPublicKey, BlsSignature}, - types::ModuleId, + types::{Chain, ModuleId}, }; use tonic::transport::{Channel, ClientTlsConfig}; @@ -15,12 +17,13 @@ use crate::proto::v1::{ #[derive(Clone, Debug)] pub struct DirkClient { + chain: Chain, channel: Channel, wallet: String, } impl DirkClient { - pub async fn new_from_config(config: DirkConfig) -> Result { + pub async fn new_from_config(chain: Chain, config: DirkConfig) -> eyre::Result { let mut tls_config = ClientTlsConfig::new().identity(config.client_cert); if let Some(ca) = config.cert_auth { @@ -39,7 +42,7 @@ impl DirkClient { .await .map_err(|e| eyre::eyre!("Couldn't connect to Dirk: {e}"))?; - Ok(Self { channel, wallet: config.wallet }) + Ok(Self { chain, channel, wallet: config.wallet }) } async fn list_accounts(&self) -> eyre::Result> { @@ -55,16 +58,30 @@ impl DirkClient { Ok(pubkeys_response.into_inner().accounts) } - pub async fn get_pubkeys(&self) -> eyre::Result> { + pub async fn get_pubkeys(&self) -> eyre::Result> { let accounts = self.list_accounts().await?; let keys = accounts .iter() .filter_map(|account| { if account.name == format!("{}/consensus", self.wallet.clone()) { - Some(ConsensusProxyMap::new(BlsPublicKey::from(FixedBytes::from_slice( - &account.public_key, - )))) + Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) + } else { + None + } + }) + .collect(); + Ok(keys) + } + + pub async fn get_proxy_pubkeys(&self) -> eyre::Result> { + let accounts = self.list_accounts().await?; + + let keys = accounts + .iter() + .filter_map(|account| { + if account.name == format!("{}/consensus", self.wallet.clone()) { + Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) } else { None } @@ -102,7 +119,7 @@ impl DirkClient { &self, module_id: ModuleId, consensus_pubkey: BlsPublicKey, - ) -> Result, eyre::Error> { + ) -> eyre::Result> { let uuid = uuid::Uuid::new_v4(); let mut client = AccountManagerClient::new(self.channel.clone()); @@ -138,15 +155,16 @@ impl DirkClient { }) } - async fn request_signature( + pub async fn request_signature( &self, - signer_id: SignerId, - domain: [u8; 32], + pubkey: BlsPublicKey, object_root: [u8; 32], - ) -> Result { + ) -> eyre::Result { + let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); + let mut signer_client = SignerClient::new(self.channel.clone()); let sign_request = tonic::Request::new(SignRequest { - id: Some(signer_id), + id: Some(SignerId::PublicKey(pubkey.to_vec())), domain: domain.to_vec(), data: object_root.to_vec(), }); @@ -158,26 +176,4 @@ impl DirkClient { Ok(BlsSignature::from(FixedBytes::from_slice(&sign_response.into_inner().signature))) } - - pub async fn request_consensus_signature( - &self, - domain: [u8; 32], - object_root: [u8; 32], - ) -> eyre::Result { - self.request_signature( - SignerId::Account(format!("{}/consensus", self.wallet.clone())), - domain, - object_root, - ) - .await - } - - pub async fn request_proxy_bls_signature( - &self, - bls_key: &BlsPublicKey, - domain: [u8; 32], - object_root: [u8; 32], - ) -> eyre::Result { - self.request_signature(SignerId::PublicKey(bls_key.0.to_vec()), domain, object_root).await - } } diff --git a/crates/signer/src/manager.rs b/crates/signer/src/manager.rs index 69224b23..755bcb3c 100644 --- a/crates/signer/src/manager.rs +++ b/crates/signer/src/manager.rs @@ -17,7 +17,8 @@ use tree_hash::TreeHash; use crate::error::SignerModuleError; -pub struct SigningManager { +#[derive(Clone)] +pub struct LocalSigningManager { pub chain: Chain, proxy_store: Option, consensus_signers: HashMap, @@ -29,7 +30,7 @@ pub struct SigningManager { proxy_pubkeys_ecdsa: HashMap>, } -impl SigningManager { +impl LocalSigningManager { pub fn new(chain: Chain, proxy_store: Option) -> eyre::Result { let mut manager = Self { chain, @@ -276,8 +277,8 @@ mod tests { static ref MODULE_ID: ModuleId = ModuleId("SAMPLE_MODULE".to_string()); } - fn init_signing_manager() -> (SigningManager, BlsPublicKey) { - let mut signing_manager = SigningManager::new(CHAIN, None).unwrap(); + fn init_signing_manager() -> (LocalSigningManager, BlsPublicKey) { + let mut signing_manager = LocalSigningManager::new(CHAIN, None).unwrap(); let consensus_signer = ConsensusSigner::new_random(); let consensus_pk = consensus_signer.pubkey(); diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 060ed9d7..c34c2459 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -16,13 +16,12 @@ use cb_common::{ GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REQUEST_SIGNATURE_PATH, STATUS_PATH, }, request::{ - EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, SignConsensusRequest, - SignProxyRequest, SignRequest, + ConsensusProxyMap, EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, + SignConsensusRequest, SignProxyRequest, SignRequest, }, }, config::StartSignerConfig, - constants::{COMMIT_BOOST_DOMAIN, COMMIT_BOOST_VERSION}, - signature::compute_domain, + constants::COMMIT_BOOST_VERSION, types::{Jwt, ModuleId}, }; use cb_metrics::provider::MetricsProvider; @@ -35,22 +34,61 @@ use uuid::Uuid; use crate::{ dirk::DirkClient, error::SignerModuleError, - manager::SigningManager, + manager::LocalSigningManager, metrics::{uri_to_tag, SIGNER_METRICS_REGISTRY, SIGNER_STATUS}, }; /// Implements the Signer API and provides a service for signing requests pub struct SigningService; +#[derive(Clone)] +pub enum SigningManager { + Local(Arc>), + Dirk(DirkClient), +} + +impl SigningManager { + /// Amount of consensus signers available + pub async fn available_consensus_signers(&self) -> eyre::Result { + match self { + SigningManager::Local(manager) => Ok(manager.read().await.consensus_pubkeys().len()), + SigningManager::Dirk(dirk) => Ok(dirk.get_pubkeys().await?.len()), + } + } + + /// Amount of proxy signers available + pub async fn available_proxy_signers(&self) -> eyre::Result { + match self { + SigningManager::Local(manager) => { + let proxies = manager.read().await.proxies().clone(); + Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) + } + SigningManager::Dirk(dirk) => Ok(dirk.get_proxy_pubkeys().await?.len()), + } + } + + pub async fn get_consensus_proxy_maps( + &self, + module_id: &ModuleId, + ) -> eyre::Result> { + match self { + SigningManager::Local(local_manager) => { + local_manager.read().await.get_consensus_proxy_maps(module_id) + } + SigningManager::Dirk(dirk_manager) => { + dirk_manager.get_consensus_proxy_maps(module_id).await + } + } + } +} + #[derive(Clone)] struct SigningState { /// Manager handling different signing methods - manager: Arc>, + manager: SigningManager, /// Map of JWTs to module ids. This also acts as registry of all modules /// running jwts: Arc>, - /// Dirk settings - dirk: Option, } impl SigningService { @@ -60,36 +98,43 @@ impl SigningService { return Ok(()); } - let proxy_store = if let Some(store) = config.store { - Some(store.init_from_env()?) - } else { - warn!("Proxy store not configured. Proxies keys and delegations will not be persisted"); - None - }; + let module_ids: Vec = config.jwts.left_values().cloned().map(Into::into).collect(); - let mut manager = SigningManager::new(config.chain, proxy_store)?; + let state = match &config.dirk { + Some(dirk) => SigningState { + manager: SigningManager::Dirk( + DirkClient::new_from_config(config.chain, dirk.clone()).await?, + ), + jwts: config.jwts.into(), + }, + None => { + let proxy_store = if let Some(store) = config.store { + Some(store.init_from_env()?) + } else { + warn!("Proxy store not configured. Proxies keys and delegations will not be persisted"); + None + }; + + let mut local_manager = LocalSigningManager::new(config.chain, proxy_store)?; + + if let Some(loader) = config.loader { + for signer in loader.load_keys()? { + local_manager.add_consensus_signer(signer); + } + } - if let Some(loader) = config.loader { - for signer in loader.load_keys()? { - manager.add_consensus_signer(signer); + SigningState { + manager: SigningManager::Local(Arc::new(RwLock::new(local_manager))), + jwts: config.jwts.into(), + } } - } - let module_ids: Vec = config.jwts.left_values().cloned().map(Into::into).collect(); + }; - let loaded_consensus = manager.consensus_pubkeys().len(); - let proxies = manager.proxies(); - let loaded_proxies = proxies.bls_signers.len() + proxies.ecdsa_signers.len(); + let loaded_consensus = state.manager.available_consensus_signers().await?; + let loaded_proxies = state.manager.available_proxy_signers().await?; info!(version = COMMIT_BOOST_VERSION, modules =? module_ids, port =? config.server_port, loaded_consensus, loaded_proxies, "Starting signing service"); - let state = SigningState { - manager: RwLock::new(manager).into(), - jwts: config.jwts.into(), - dirk: match config.dirk { - Some(dirk) => Some(DirkClient::new_from_config(dirk).await?), - None => None, - }, - }; SigningService::init_metrics()?; let app = axum::Router::new() @@ -155,18 +200,11 @@ async fn handle_get_pubkeys( debug!(event = "get_pubkeys", ?req_id, "New request"); - let keys = match state.dirk { - Some(dirk) => dirk - .get_consensus_proxy_maps(&module_id) - .await - .map_err(|e| SignerModuleError::Internal(e.to_string()))?, - None => { - let signing_manager = state.manager.read().await; - signing_manager - .get_consensus_proxy_maps(&module_id) - .map_err(|err| SignerModuleError::Internal(err.to_string()))? - } - }; + let keys = state + .manager + .get_consensus_proxy_maps(&module_id) + .await + .map_err(|err| SignerModuleError::Internal(err.to_string()))?; let res = GetPubkeysResponse { keys }; @@ -183,61 +221,55 @@ async fn handle_request_signature( debug!(event = "request_signature", ?module_id, ?req_id, "New request"); - let signing_manager = state.manager.read().await; - - let signature_response = match state.dirk { - Some(dirk) => match request { - SignRequest::Consensus(SignConsensusRequest { object_root, .. }) => dirk - .request_consensus_signature( - compute_domain(signing_manager.chain, COMMIT_BOOST_DOMAIN), - object_root, - ) + let response = match state.manager { + SigningManager::Local(local_manager) => match request { + SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => local_manager + .read() .await - .map(|sig| Json(sig).into_response()) - .map_err(|e| SignerModuleError::Internal(e.to_string())), - SignRequest::ProxyBls(SignProxyRequest { pubkey: bls_pk, object_root }) => dirk - .request_proxy_bls_signature( - &bls_pk, - compute_domain(signing_manager.chain, COMMIT_BOOST_DOMAIN), - object_root, - ) + .sign_consensus(&pubkey, &object_root) .await .map(|sig| Json(sig).into_response()) - .map_err(|e| SignerModuleError::Internal(e.to_string())), - _ => { - error!("ECDSA proxy sign request not supported with Dirk"); - Err(SignerModuleError::DirkNotSupported) + .map_err(|err| SignerModuleError::Internal(err.to_string())), + SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { + local_manager + .read() + .await + .sign_proxy_bls(&bls_key, &object_root) + .await + .map(|sig| Json(sig).into_response()) + .map_err(|err| SignerModuleError::Internal(err.to_string())) } - }, - None => match request { - SignRequest::Consensus(SignConsensusRequest { pubkey, object_root }) => signing_manager - .sign_consensus(&pubkey, &object_root) - .await - .map(|sig| Json(sig).into_response()), - SignRequest::ProxyBls(SignProxyRequest { pubkey: bls_pk, object_root }) => { - if !signing_manager.has_proxy_bls_for_module(&bls_pk, &module_id) { - return Err(SignerModuleError::UnknownProxySigner(bls_pk.to_vec())); - } - - signing_manager - .sign_proxy_bls(&bls_pk, &object_root) + SignRequest::ProxyEcdsa(SignProxyRequest { object_root, pubkey: ecdsa_key }) => { + local_manager + .read() + .await + .sign_proxy_ecdsa(&ecdsa_key, &object_root) .await .map(|sig| Json(sig).into_response()) + .map_err(|err| SignerModuleError::Internal(err.to_string())) } - SignRequest::ProxyEcdsa(SignProxyRequest { pubkey: ecdsa_pk, object_root }) => { - if !signing_manager.has_proxy_ecdsa_for_module(&ecdsa_pk, &module_id) { - return Err(SignerModuleError::UnknownProxySigner(ecdsa_pk.to_vec())); - } - - signing_manager - .sign_proxy_ecdsa(&ecdsa_pk, &object_root) + }, + SigningManager::Dirk(dirk_manager) => match request { + SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => dirk_manager + .request_signature(pubkey, object_root) + .await + .map(|sig| Json(sig).into_response()) + .map_err(|err| SignerModuleError::Internal(err.to_string())), + SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { + dirk_manager + .request_signature(bls_key, object_root) .await .map(|sig| Json(sig).into_response()) + .map_err(|err| SignerModuleError::Internal(err.to_string())) + } + SignRequest::ProxyEcdsa(_) => { + error!("ECDSA proxy sign request not supported with Dirk"); + Err(SignerModuleError::DirkNotSupported) } }, }; - Ok(signature_response?) + response } async fn handle_generate_proxy( @@ -249,39 +281,35 @@ async fn handle_generate_proxy( debug!(event = "generate_proxy", module_id=?module_id, ?req_id, "New request"); - let response = match state.dirk { - Some(dirk) => match request.scheme { - EncryptionScheme::Bls => { - let proxy_delegation = dirk - .generate_proxy_key(module_id, request.consensus_pubkey) - .await - .map_err(|e| SignerModuleError::Internal(e.to_string()))?; - Json(proxy_delegation).into_response() - } + let response = match state.manager { + SigningManager::Local(local_manager) => match request.scheme { + EncryptionScheme::Bls => local_manager + .write() + .await + .create_proxy_bls(module_id, request.consensus_pubkey) + .await + .map(|proxy_delegation| Json(proxy_delegation).into_response()) + .map_err(|err| SignerModuleError::Internal(err.to_string())), + EncryptionScheme::Ecdsa => local_manager + .write() + .await + .create_proxy_ecdsa(module_id, request.consensus_pubkey) + .await + .map(|proxy_delegation| Json(proxy_delegation).into_response()) + .map_err(|err| SignerModuleError::Internal(err.to_string())), + }, + SigningManager::Dirk(dirk_manager) => match request.scheme { + EncryptionScheme::Bls => dirk_manager + .generate_proxy_key(module_id, request.consensus_pubkey) + .await + .map(|proxy_delegation| Json(proxy_delegation).into_response()) + .map_err(|err| SignerModuleError::Internal(err.to_string())), EncryptionScheme::Ecdsa => { error!("ECDSA proxy generation not supported with Dirk"); - return Err(SignerModuleError::DirkNotSupported); + Err(SignerModuleError::DirkNotSupported) } }, - None => { - let mut signing_manager = state.manager.write().await; - - match request.scheme { - EncryptionScheme::Bls => { - let proxy_delegation = signing_manager - .create_proxy_bls(module_id, request.consensus_pubkey) - .await?; - Json(proxy_delegation).into_response() - } - EncryptionScheme::Ecdsa => { - let proxy_delegation = signing_manager - .create_proxy_ecdsa(module_id, request.consensus_pubkey) - .await?; - Json(proxy_delegation).into_response() - } - } - } }; - Ok(response) + response } From 4d39e7e223490932409da2040ca0ca3394e05772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 3 Jan 2025 17:13:52 -0300 Subject: [PATCH 010/104] More refactor --- crates/signer/src/lib.rs | 1 - crates/signer/src/{ => manager}/dirk.rs | 4 +- .../src/{manager.rs => manager/local.rs} | 0 crates/signer/src/manager/mod.rs | 52 +++++++++++++++++++ crates/signer/src/service.rs | 50 ++---------------- 5 files changed, 58 insertions(+), 49 deletions(-) rename crates/signer/src/{ => manager}/dirk.rs (99%) rename crates/signer/src/{manager.rs => manager/local.rs} (100%) create mode 100644 crates/signer/src/manager/mod.rs diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index b3b7ffee..85b32f47 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -1,5 +1,4 @@ mod constants; -mod dirk; pub mod error; pub mod manager; mod metrics; diff --git a/crates/signer/src/dirk.rs b/crates/signer/src/manager/dirk.rs similarity index 99% rename from crates/signer/src/dirk.rs rename to crates/signer/src/manager/dirk.rs index 8fab2f33..64d8991d 100644 --- a/crates/signer/src/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -16,13 +16,13 @@ use crate::proto::v1::{ }; #[derive(Clone, Debug)] -pub struct DirkClient { +pub struct DirkManager { chain: Chain, channel: Channel, wallet: String, } -impl DirkClient { +impl DirkManager { pub async fn new_from_config(chain: Chain, config: DirkConfig) -> eyre::Result { let mut tls_config = ClientTlsConfig::new().identity(config.client_cert); diff --git a/crates/signer/src/manager.rs b/crates/signer/src/manager/local.rs similarity index 100% rename from crates/signer/src/manager.rs rename to crates/signer/src/manager/local.rs diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs new file mode 100644 index 00000000..15856c0e --- /dev/null +++ b/crates/signer/src/manager/mod.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; + +use cb_common::{commit::request::ConsensusProxyMap, types::ModuleId}; +use dirk::DirkManager; +use local::LocalSigningManager; +use tokio::sync::RwLock; + +pub mod dirk; +pub mod local; + +#[derive(Clone)] +pub enum SigningManager { + Local(Arc>), + Dirk(DirkManager), +} + +impl SigningManager { + /// Amount of consensus signers available + pub async fn available_consensus_signers(&self) -> eyre::Result { + match self { + SigningManager::Local(local_manager) => { + Ok(local_manager.read().await.consensus_pubkeys().len()) + } + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.get_pubkeys().await?.len()), + } + } + + /// Amount of proxy signers available + pub async fn available_proxy_signers(&self) -> eyre::Result { + match self { + SigningManager::Local(local_manager) => { + let proxies = local_manager.read().await.proxies().clone(); + Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) + } + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.get_proxy_pubkeys().await?.len()), + } + } + + pub async fn get_consensus_proxy_maps( + &self, + module_id: &ModuleId, + ) -> eyre::Result> { + match self { + SigningManager::Local(local_manager) => { + local_manager.read().await.get_consensus_proxy_maps(module_id) + } + SigningManager::Dirk(dirk_manager) => { + dirk_manager.get_consensus_proxy_maps(module_id).await + } + } + } +} diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index c34c2459..36e21397 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -16,8 +16,8 @@ use cb_common::{ GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REQUEST_SIGNATURE_PATH, STATUS_PATH, }, request::{ - ConsensusProxyMap, EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, - SignConsensusRequest, SignProxyRequest, SignRequest, + EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, SignConsensusRequest, + SignProxyRequest, SignRequest, }, }, config::StartSignerConfig, @@ -32,56 +32,14 @@ use tracing::{debug, error, info, warn}; use uuid::Uuid; use crate::{ - dirk::DirkClient, error::SignerModuleError, - manager::LocalSigningManager, + manager::{dirk::DirkManager, local::LocalSigningManager, SigningManager}, metrics::{uri_to_tag, SIGNER_METRICS_REGISTRY, SIGNER_STATUS}, }; /// Implements the Signer API and provides a service for signing requests pub struct SigningService; -#[derive(Clone)] -pub enum SigningManager { - Local(Arc>), - Dirk(DirkClient), -} - -impl SigningManager { - /// Amount of consensus signers available - pub async fn available_consensus_signers(&self) -> eyre::Result { - match self { - SigningManager::Local(manager) => Ok(manager.read().await.consensus_pubkeys().len()), - SigningManager::Dirk(dirk) => Ok(dirk.get_pubkeys().await?.len()), - } - } - - /// Amount of proxy signers available - pub async fn available_proxy_signers(&self) -> eyre::Result { - match self { - SigningManager::Local(manager) => { - let proxies = manager.read().await.proxies().clone(); - Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) - } - SigningManager::Dirk(dirk) => Ok(dirk.get_proxy_pubkeys().await?.len()), - } - } - - pub async fn get_consensus_proxy_maps( - &self, - module_id: &ModuleId, - ) -> eyre::Result> { - match self { - SigningManager::Local(local_manager) => { - local_manager.read().await.get_consensus_proxy_maps(module_id) - } - SigningManager::Dirk(dirk_manager) => { - dirk_manager.get_consensus_proxy_maps(module_id).await - } - } - } -} - #[derive(Clone)] struct SigningState { /// Manager handling different signing methods @@ -103,7 +61,7 @@ impl SigningService { let state = match &config.dirk { Some(dirk) => SigningState { manager: SigningManager::Dirk( - DirkClient::new_from_config(config.chain, dirk.clone()).await?, + DirkManager::new_from_config(config.chain, dirk.clone()).await?, ), jwts: config.jwts.into(), }, From 8654e5fd81757950d3a4213cbad13d421c71d4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 3 Jan 2025 17:16:39 -0300 Subject: [PATCH 011/104] Fix format --- crates/cli/src/docker_init.rs | 7 +- crates/common/src/config/signer.rs | 16 +-- crates/signer/src/proto/v1.rs | 205 +++++++++-------------------- crates/signer/src/service.rs | 12 +- 4 files changed, 77 insertions(+), 163 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index eec13acc..31b3fb13 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -158,10 +158,9 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu // depends_on let mut module_dependencies = IndexMap::new(); - module_dependencies.insert( - "cb_signer".into(), - DependsCondition { condition: "service_healthy".into() }, - ); + module_dependencies.insert("cb_signer".into(), DependsCondition { + condition: "service_healthy".into(), + }); Service { container_name: Some(module_cid.clone()), diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 31caf0de..987206fa 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -104,16 +104,12 @@ impl StartSignerConfig { server_domain, .. }) => { - let cert_path = load_env_var(SIGNER_DIRK_CERT_ENV) - .map(|path| PathBuf::from(path)) - .unwrap_or(cert_path); - let key_path = load_env_var(SIGNER_DIRK_KEY_ENV) - .map(|path| PathBuf::from(path)) - .unwrap_or(key_path); - let ca_cert_path = load_env_var(SIGNER_DIRK_CA_CERT_ENV) - .map(|path| PathBuf::from(path)) - .ok() - .or(ca_cert_path); + let cert_path = + load_env_var(SIGNER_DIRK_CERT_ENV).map(PathBuf::from).unwrap_or(cert_path); + let key_path = + load_env_var(SIGNER_DIRK_KEY_ENV).map(PathBuf::from).unwrap_or(key_path); + let ca_cert_path = + load_env_var(SIGNER_DIRK_CA_CERT_ENV).map(PathBuf::from).ok().or(ca_cert_path); Ok(StartSignerConfig { chain: config.chain, diff --git a/crates/signer/src/proto/v1.rs b/crates/signer/src/proto/v1.rs index 36984aa0..ba8012c3 100644 --- a/crates/signer/src/proto/v1.rs +++ b/crates/signer/src/proto/v1.rs @@ -24,7 +24,8 @@ impl ResponseState { /// String value of the enum field names used in the ProtoBuf definition. /// /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. + /// (if the ProtoBuf definition does not change) and safe for programmatic + /// use. pub fn as_str_name(&self) -> &'static str { match self { Self::Unknown => "UNKNOWN", @@ -89,10 +90,9 @@ pub mod lister_client { dead_code, missing_docs, clippy::wildcard_imports, - clippy::let_unit_value, + clippy::let_unit_value )] - use tonic::codegen::*; - use tonic::codegen::http::Uri; + use tonic::codegen::{http::Uri, *}; #[derive(Debug, Clone)] pub struct ListerClient { inner: tonic::client::Grpc, @@ -136,16 +136,15 @@ pub mod lister_client { >::ResponseBody, >, >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, + >>::Error: + Into + std::marker::Send + std::marker::Sync, { ListerClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// - /// This requires the server to support it otherwise it might respond with an - /// error. + /// This requires the server to support it otherwise it might respond + /// with an error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); @@ -176,18 +175,11 @@ pub mod lister_client { pub async fn list_accounts( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/v1.Lister/ListAccounts"); let mut req = request.into_request(); @@ -247,10 +239,9 @@ pub mod account_manager_client { dead_code, missing_docs, clippy::wildcard_imports, - clippy::let_unit_value, + clippy::let_unit_value )] - use tonic::codegen::*; - use tonic::codegen::http::Uri; + use tonic::codegen::{http::Uri, *}; #[derive(Debug, Clone)] pub struct AccountManagerClient { inner: tonic::client::Grpc, @@ -294,16 +285,15 @@ pub mod account_manager_client { >::ResponseBody, >, >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, + >>::Error: + Into + std::marker::Send + std::marker::Sync, { AccountManagerClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// - /// This requires the server to support it otherwise it might respond with an - /// error. + /// This requires the server to support it otherwise it might respond + /// with an error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); @@ -334,18 +324,11 @@ pub mod account_manager_client { pub async fn unlock( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/v1.AccountManager/Unlock"); let mut req = request.into_request(); @@ -355,18 +338,11 @@ pub mod account_manager_client { pub async fn lock( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/v1.AccountManager/Lock"); let mut req = request.into_request(); @@ -376,25 +352,14 @@ pub mod account_manager_client { pub async fn generate( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/v1.AccountManager/Generate", - ); + let path = http::uri::PathAndQuery::from_static("/v1.AccountManager/Generate"); let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("v1.AccountManager", "Generate")); + req.extensions_mut().insert(GrpcMethod::new("v1.AccountManager", "Generate")); self.inner.unary(req, path, codec).await } } @@ -521,10 +486,9 @@ pub mod signer_client { dead_code, missing_docs, clippy::wildcard_imports, - clippy::let_unit_value, + clippy::let_unit_value )] - use tonic::codegen::*; - use tonic::codegen::http::Uri; + use tonic::codegen::{http::Uri, *}; #[derive(Debug, Clone)] pub struct SignerClient { inner: tonic::client::Grpc, @@ -568,16 +532,15 @@ pub mod signer_client { >::ResponseBody, >, >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, + >>::Error: + Into + std::marker::Send + std::marker::Sync, { SignerClient::new(InterceptedService::new(inner, interceptor)) } /// Compress requests with the given encoding. /// - /// This requires the server to support it otherwise it might respond with an - /// error. + /// This requires the server to support it otherwise it might respond + /// with an error. #[must_use] pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.inner = self.inner.send_compressed(encoding); @@ -609,14 +572,9 @@ pub mod signer_client { &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/v1.Signer/Sign"); let mut req = request.into_request(); @@ -626,18 +584,10 @@ pub mod signer_client { pub async fn multisign( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/v1.Signer/Multisign"); let mut req = request.into_request(); @@ -648,66 +598,39 @@ pub mod signer_client { &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/v1.Signer/SignBeaconAttestation", - ); + let path = http::uri::PathAndQuery::from_static("/v1.Signer/SignBeaconAttestation"); let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("v1.Signer", "SignBeaconAttestation")); + req.extensions_mut().insert(GrpcMethod::new("v1.Signer", "SignBeaconAttestation")); self.inner.unary(req, path, codec).await } pub async fn sign_beacon_attestations( &mut self, request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/v1.Signer/SignBeaconAttestations", - ); + let path = http::uri::PathAndQuery::from_static("/v1.Signer/SignBeaconAttestations"); let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("v1.Signer", "SignBeaconAttestations")); + req.extensions_mut().insert(GrpcMethod::new("v1.Signer", "SignBeaconAttestations")); self.inner.unary(req, path, codec).await } pub async fn sign_beacon_proposal( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/v1.Signer/SignBeaconProposal", - ); + let path = http::uri::PathAndQuery::from_static("/v1.Signer/SignBeaconProposal"); let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("v1.Signer", "SignBeaconProposal")); + req.extensions_mut().insert(GrpcMethod::new("v1.Signer", "SignBeaconProposal")); self.inner.unary(req, path, codec).await } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 36e21397..fcb47678 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -179,7 +179,7 @@ async fn handle_request_signature( debug!(event = "request_signature", ?module_id, ?req_id, "New request"); - let response = match state.manager { + match state.manager { SigningManager::Local(local_manager) => match request { SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => local_manager .read() @@ -225,9 +225,7 @@ async fn handle_request_signature( Err(SignerModuleError::DirkNotSupported) } }, - }; - - response + } } async fn handle_generate_proxy( @@ -239,7 +237,7 @@ async fn handle_generate_proxy( debug!(event = "generate_proxy", module_id=?module_id, ?req_id, "New request"); - let response = match state.manager { + match state.manager { SigningManager::Local(local_manager) => match request.scheme { EncryptionScheme::Bls => local_manager .write() @@ -267,7 +265,5 @@ async fn handle_generate_proxy( Err(SignerModuleError::DirkNotSupported) } }, - }; - - response + } } From 8ff394d098805d9c4824ceb6d37a4d56573e03c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 3 Jan 2025 17:30:55 -0300 Subject: [PATCH 012/104] Update DA_COMMIT example --- examples/da_commit/src/main.rs | 73 ++++++++++++++-------------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index 661b9c93..2b28d28c 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -33,11 +33,11 @@ struct DaCommitService { #[derive(Debug, Deserialize)] struct ExtraConfig { sleep_secs: u64, - #[serde(default = "default_delegation")] - use_delegation_keys: bool, + #[serde(default = "default_ecdsa")] + use_ecdsa_keys: bool, } -fn default_delegation() -> bool { +fn default_ecdsa() -> bool { true } @@ -51,41 +51,34 @@ impl DaCommitService { let pubkey = pubkeys.first().ok_or_eyre("no key available")?.consensus; info!("Registered validator {pubkey}"); - if self.config.extra.use_delegation_keys { - let proxy_delegation_bls = - self.config.signer_client.generate_proxy_key_bls(pubkey).await?; - info!("Obtained a BLS proxy delegation:\n{proxy_delegation_bls}"); - let proxy_bls = proxy_delegation_bls.message.proxy; + let proxy_delegation_bls = self.config.signer_client.generate_proxy_key_bls(pubkey).await?; + info!("Obtained a BLS proxy delegation:\n{proxy_delegation_bls}"); + let proxy_bls = proxy_delegation_bls.message.proxy; + let proxy_ecdsa = if self.config.extra.use_ecdsa_keys { let proxy_delegation_ecdsa = self.config.signer_client.generate_proxy_key_ecdsa(pubkey).await?; info!("Obtained an ECDSA proxy delegation:\n{proxy_delegation_ecdsa}"); - let proxy_ecdsa = proxy_delegation_ecdsa.message.proxy; - - let mut data = 0; - - loop { - self.send_multiple_requests(data, pubkey, proxy_bls, proxy_ecdsa).await?; - sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; - data += 1; - } + Some(proxy_delegation_ecdsa.message.proxy) } else { - let mut data = 0; + None + }; - loop { - self.send_request(data, pubkey).await?; - sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; - data += 1; - } + let mut data = 0; + + loop { + self.send_request(data, pubkey, proxy_bls, proxy_ecdsa).await?; + sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; + data += 1; } } - pub async fn send_multiple_requests( + pub async fn send_request( &self, data: u64, pubkey: BlsPublicKey, proxy_bls: BlsPublicKey, - proxy_ecdsa: EcdsaPublicKey, + proxy_ecdsa: Option, ) -> Result<()> { let datagram = Datagram { data }; @@ -96,31 +89,23 @@ impl DaCommitService { let proxy_signature_bls = self.config.signer_client.request_proxy_signature_bls(proxy_request_bls); - let proxy_request_ecdsa = SignProxyRequest::builder(proxy_ecdsa).with_msg(&datagram); - let proxy_signature_ecdsa = - self.config.signer_client.request_proxy_signature_ecdsa(proxy_request_ecdsa); + let proxy_signature_ecdsa = proxy_ecdsa.map(|proxy_ecdsa| { + let proxy_request_ecdsa = SignProxyRequest::builder(proxy_ecdsa).with_msg(&datagram); + self.config.signer_client.request_proxy_signature_ecdsa(proxy_request_ecdsa) + }); - let (signature, proxy_signature_bls, proxy_signature_ecdsa) = { - let res = tokio::join!(signature, proxy_signature_bls, proxy_signature_ecdsa); - (res.0?, res.1?, res.2?) + let (signature, proxy_signature_bls) = { + let res = tokio::join!(signature, proxy_signature_bls); + (res.0?, res.1?) }; info!("Proposer commitment (consensus): {}", signature); info!("Proposer commitment (proxy BLS): {}", proxy_signature_bls); - info!("Proposer commitment (proxy ECDSA): {}", proxy_signature_ecdsa); - - SIG_RECEIVED_COUNTER.inc(); - Ok(()) - } - - pub async fn send_request(&self, data: u64, pubkey: BlsPublicKey) -> Result<()> { - let datagram = Datagram { data }; - - let request = SignConsensusRequest::builder(pubkey).with_msg(&datagram); - let signature = self.config.signer_client.request_consensus_signature(request).await?; - - info!("Proposer commitment (consensus): {}", signature); + if let Some(proxy_signature_ecdsa) = proxy_signature_ecdsa { + let proxy_signature_ecdsa = proxy_signature_ecdsa.await?; + info!("Proposer commitment (proxy ECDSA): {}", proxy_signature_ecdsa); + } SIG_RECEIVED_COUNTER.inc(); From 4d4449ac22ea4cab7fcb7079a57229bdf4699c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 12:09:59 -0300 Subject: [PATCH 013/104] Support multiple wallets --- crates/common/src/config/signer.rs | 10 +-- crates/signer/src/manager/dirk.rs | 109 +++++++++++++++++++---------- crates/signer/src/manager/mod.rs | 4 +- 3 files changed, 79 insertions(+), 44 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 987206fa..4204e3f7 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -46,8 +46,8 @@ pub enum SignerConfig { cert_path: PathBuf, /// Path to the client key key_path: PathBuf, - /// Wallet to use - wallet: String, + /// Wallets to use. Each wallet should have a `wallet/consensus` account + wallets: Vec, /// Path to the CA certificate ca_cert_path: Option, /// Domain name of the server to use in TLS verification @@ -62,7 +62,7 @@ fn default_signer() -> String { #[derive(Clone, Debug)] pub struct DirkConfig { pub url: Url, - pub wallet: String, + pub wallets: Vec, pub client_cert: Identity, pub cert_auth: Option, pub server_domain: Option, @@ -99,7 +99,7 @@ impl StartSignerConfig { url, cert_path, key_path, - wallet, + wallets, ca_cert_path, server_domain, .. @@ -119,7 +119,7 @@ impl StartSignerConfig { store: None, dirk: Some(DirkConfig { url, - wallet, + wallets, client_cert: Identity::from_pem( std::fs::read_to_string(cert_path)?, std::fs::read_to_string(key_path)?, diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 64d8991d..c5a139fb 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -19,7 +19,7 @@ use crate::proto::v1::{ pub struct DirkManager { chain: Chain, channel: Channel, - wallet: String, + wallets: Vec, } impl DirkManager { @@ -42,14 +42,14 @@ impl DirkManager { .await .map_err(|e| eyre::eyre!("Couldn't connect to Dirk: {e}"))?; - Ok(Self { chain, channel, wallet: config.wallet }) + Ok(Self { chain, channel, wallets: config.wallets }) } - async fn list_accounts(&self) -> eyre::Result> { + async fn get_all_accounts(&self) -> eyre::Result> { let mut client = ListerClient::new(self.channel.clone()); let pubkeys_request = - tonic::Request::new(ListAccountsRequest { paths: vec![self.wallet.clone()] }); - let pubkeys_response = client.list_accounts(pubkeys_request).await.unwrap(); + tonic::Request::new(ListAccountsRequest { paths: self.wallets.clone() }); + let pubkeys_response = client.list_accounts(pubkeys_request).await?; if pubkeys_response.get_ref().state() != ResponseState::Succeeded { eyre::bail!("Get pubkeys request failed".to_string()); @@ -58,61 +58,91 @@ impl DirkManager { Ok(pubkeys_response.into_inner().accounts) } - pub async fn get_pubkeys(&self) -> eyre::Result> { - let accounts = self.list_accounts().await?; + async fn get_pubkey_wallet(&self, pubkey: BlsPublicKey) -> eyre::Result> { + let accounts = self.get_all_accounts().await?; - let keys = accounts + for account in accounts { + if account.public_key == pubkey.to_vec() { + return Ok(Some(account.name)); + } + } + + Ok(None) + } + + pub async fn consensus_pubkeys(&self) -> eyre::Result> { + let accounts = self.get_all_accounts().await?; + + let expected_accounts: Vec = + self.wallets.iter().map(|wallet| format!("{wallet}/consensus")).collect(); + + Ok(accounts .iter() .filter_map(|account| { - if account.name == format!("{}/consensus", self.wallet.clone()) { + if expected_accounts.contains(&account.name) { Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) } else { None } }) - .collect(); - Ok(keys) + .collect()) } - pub async fn get_proxy_pubkeys(&self) -> eyre::Result> { - let accounts = self.list_accounts().await?; + pub async fn proxies(&self) -> eyre::Result> { + let accounts = self.get_all_accounts().await?; - let keys = accounts + Ok(accounts .iter() .filter_map(|account| { - if account.name == format!("{}/consensus", self.wallet.clone()) { + let wallet = account.name.split_once("/")?.0; + if self.wallets.contains(&wallet.to_string()) + && account.name != format!("{wallet}/consensus") + { Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) } else { None } }) - .collect(); - Ok(keys) + .collect()) } pub async fn get_consensus_proxy_maps( &self, module_id: &ModuleId, ) -> eyre::Result> { - let accounts = self.list_accounts().await?; + let accounts = self.get_all_accounts().await?; - let consensus_pubkey = accounts - .iter() - .find(|account| account.name == format!("{}/consensus", self.wallet.clone())) - .map(|account| BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) - .ok_or_else(|| eyre::eyre!("No consensus key found"))?; + let mut proxy_maps = Vec::new(); - let proxy_keys = accounts - .iter() - .filter(|account| account.name.starts_with(&format!("{}/{module_id}/", self.wallet))) - .map(|account| BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) - .collect::>(); - - Ok(vec![ConsensusProxyMap { - consensus: consensus_pubkey, - proxy_bls: proxy_keys, - proxy_ecdsa: vec![], - }]) + for wallet in self.wallets.iter() { + let Some(consensus_key) = accounts.iter().find_map(|account| { + if account.name == format!("{wallet}/consensus") { + Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) + } else { + None + } + }) else { + continue; + }; + + let proxy_keys = accounts + .iter() + .filter_map(|account| { + if account.name.starts_with(&format!("{wallet}/{module_id}")) { + Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) + } else { + None + } + }) + .collect::>(); + proxy_maps.push(ConsensusProxyMap { + consensus: consensus_key, + proxy_bls: proxy_keys, + proxy_ecdsa: vec![], + }); + } + + Ok(proxy_maps) } pub async fn generate_proxy_key( @@ -122,9 +152,14 @@ impl DirkManager { ) -> eyre::Result> { let uuid = uuid::Uuid::new_v4(); + let wallet = self + .get_pubkey_wallet(consensus_pubkey) + .await? + .ok_or(eyre::eyre!("Consensus public key not found"))?; + let mut client = AccountManagerClient::new(self.channel.clone()); let generate_request = tonic::Request::new(GenerateRequest { - account: format!("{}/{module_id}/{uuid}", self.wallet), + account: format!("{wallet}/{module_id}/{uuid}"), passphrase: vec![0x73, 0x65, 0x63, 0x72, 0x65, 0x74], // "secret" participants: 1, signing_threshold: 1, @@ -140,7 +175,7 @@ impl DirkManager { let mut unlock_client = AccountManagerClient::new(self.channel.clone()); let unlock_request = tonic::Request::new(UnlockAccountRequest { - account: format!("{}/{module_id}/{uuid}", self.wallet), + account: format!("{wallet}/{module_id}/{uuid}"), passphrase: vec![0x73, 0x65, 0x63, 0x72, 0x65, 0x74], // "secret" }); @@ -169,7 +204,7 @@ impl DirkManager { data: object_root.to_vec(), }); - let sign_response = signer_client.sign(sign_request).await.unwrap(); + let sign_response = signer_client.sign(sign_request).await?; if sign_response.get_ref().state() != ResponseState::Succeeded { eyre::bail!("Sign request failed"); } diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index 15856c0e..c62920fa 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -21,7 +21,7 @@ impl SigningManager { SigningManager::Local(local_manager) => { Ok(local_manager.read().await.consensus_pubkeys().len()) } - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.get_pubkeys().await?.len()), + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.consensus_pubkeys().await?.len()), } } @@ -32,7 +32,7 @@ impl SigningManager { let proxies = local_manager.read().await.proxies().clone(); Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) } - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.get_proxy_pubkeys().await?.len()), + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.proxies().await?.len()), } } From 736c37fa7e52c963cdbd1277878b01b837fb88a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 12:15:37 -0300 Subject: [PATCH 014/104] Fix fmt --- crates/signer/src/manager/dirk.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index c5a139fb..fd962a4a 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -95,8 +95,8 @@ impl DirkManager { .iter() .filter_map(|account| { let wallet = account.name.split_once("/")?.0; - if self.wallets.contains(&wallet.to_string()) - && account.name != format!("{wallet}/consensus") + if self.wallets.contains(&wallet.to_string()) && + account.name != format!("{wallet}/consensus") { Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) } else { From 8fa4c68ea8f5c4ac8dc44127942a419c5fb8fb74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 12:38:42 -0300 Subject: [PATCH 015/104] Replace signature with zero --- crates/signer/src/manager/dirk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index fd962a4a..7b6f3807 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -186,7 +186,7 @@ impl DirkManager { Ok(SignedProxyDelegation { message: ProxyDelegation { delegator: consensus_pubkey, proxy: proxy_key }, - signature: BlsSignature::random(), + signature: BlsSignature::ZERO, }) } From 334a5093e1a53c0d26af0e109b3d88a3d965eb3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 13:09:50 -0300 Subject: [PATCH 016/104] Update documentation --- config.example.toml | 22 +++++++++++++++++++++- docs/docs/get_started/configuration.md | 25 +++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/config.example.toml b/config.example.toml index caffd1ea..7ec87adb 100644 --- a/config.example.toml +++ b/config.example.toml @@ -127,8 +127,9 @@ id = "mux-relay-1" url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09fe73ccd21f88eab31d6de16194d17782e@def.xyz" # Configuration for the Signer Module, only required if any `commit` module is present, or if `pbs.with_signer = true` -# Currently two types of Signer modules are supported (only one can be used at a time): +# Currently three types of Signer modules are supported (only one can be used at a time): # - Remote: a remote Web3Signer instance +# - Dirk: a remote Dirk instance # - Local: a local Signer module # More details on the docs (https://commit-boost.github.io/commit-boost-client/get_started/configuration/#local-signer) # OPTIONAL @@ -136,6 +137,25 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # [signer.remote] # URL of the Web3Signer instance # url = "https://remote.signer.url" +# Dirk: +# [signer.dirk] +# Docker image to use for the Signer module. +# OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest +# docker_image = "ghcr.io/commit-boost/signer:latest" +# Complete URL of a Dirk gateway +# url = "https://dirk.gateway.url" +# Path to the client certificate to authenticate with Dirk +# cert_path = "/path/to/client.crt" +# Path to the client key +# key_path = "/path/to/client.key" +# Wallets to use. Each wallet should have a `wallet/consensus` account +# wallets = ["wallet1", "wallet2"] +# Path to the CA certificate that signed the Dirk server certificate +# OPTIONAL +# ca_cert_path = "/path/to/ca.crt" +# Domain name of the server to use in TLS verification, if different from the URL +# OPTIONAL +# server_domain = "server.example.com" # Local: [signer.local] # Docker image to use for the Signer module. diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 36462642..7cab5835 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -229,15 +229,36 @@ To persist proxy keys across restarts, you must enable the proxy store in the co ### Remote signer -You might choose to use an external service to sign the transactions. For now, we support Web3Signer but we're working on adding support for additional signers. +You might choose to use an external service to sign the transactions. For now, two type of remote signers are supported: Web3Signer and Dirk. -The parameters needed for the remote signer are: +#### Web3Signer + +Web3Signer implements the same API that Commit-Boost, so there's no need to setup a Signer module. The parameters needed for the remote signer are: ```toml [signer.remote] url = "https://remote.signer.url" ``` +#### Dirk + +Dirk is a distributed key management system that can be used to sign transactions. In this case the Signer module is needed as an intermediary between the modules and Dirk. The following parameters are needed: + +```toml +url = "https://dirk.gateway.url" +cert_path = "/path/to/client.crt" +key_path = "/path/to/client.key" +wallets = ["wallet1", "wallet2"] + +# Optional parameters +docker_image = "ghcr.io/commit-boost/signer:latest" +ca_cert_path = "/path/to/ca.crt", +server_domain = "server.example.com", +``` + +- `cert_path` and `key_path` are the paths to the client certificate and key used to authenticate with Dirk. +- `wallets` is a list of wallets that the Signer module will use to sign transactions. Each wallet should have a `/consensus` account which will be used as the consensus key. Generated proxy keys will be stored in `//`. + ## Custom module We currently provide a test module that needs to be built locally. To build the module run: ```bash From d8a7e98e3d42f97c730855679d5342af834eccb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 14:31:49 -0300 Subject: [PATCH 017/104] Fix grammar --- docs/docs/get_started/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 7cab5835..8d4357d8 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -233,7 +233,7 @@ You might choose to use an external service to sign the transactions. For now, t #### Web3Signer -Web3Signer implements the same API that Commit-Boost, so there's no need to setup a Signer module. The parameters needed for the remote signer are: +Web3Signer implements the same API as Commit-Boost, so there's no need to set up a Signer module. The parameters needed for the remote signer are: ```toml [signer.remote] From 8ba2c2b11a5791de49b87aa6c850a5b69c4ff03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 14:36:48 -0300 Subject: [PATCH 018/104] Remove unnecessary deps --- Cargo.lock | 76 ---------------------------------------- crates/signer/Cargo.toml | 13 ++----- 2 files changed, 2 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74db47b1..4b2d175a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1397,7 +1397,6 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tonic", - "tonic-build", "tracing", "tree_hash 0.8.0", "tree_hash_derive", @@ -2227,12 +2226,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "fnv" version = "1.0.7" @@ -3110,12 +3103,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "multimap" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" - [[package]] name = "native-tls" version = "0.2.12" @@ -3418,16 +3405,6 @@ dependencies = [ "ucd-trie", ] -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap 2.7.0", -] - [[package]] name = "pharos" version = "0.5.3" @@ -3501,16 +3478,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" -dependencies = [ - "proc-macro2", - "syn 2.0.93", -] - [[package]] name = "primitive-types" version = "0.12.2" @@ -3609,26 +3576,6 @@ dependencies = [ "prost-derive", ] -[[package]] -name = "prost-build" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" -dependencies = [ - "heck", - "itertools 0.13.0", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.93", - "tempfile", -] - [[package]] name = "prost-derive" version = "0.13.4" @@ -3642,15 +3589,6 @@ dependencies = [ "syn 2.0.93", ] -[[package]] -name = "prost-types" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" -dependencies = [ - "prost", -] - [[package]] name = "protobuf" version = "2.28.0" @@ -4841,20 +4779,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tonic-build" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "prost-types", - "quote", - "syn 2.0.93", -] - [[package]] name = "tower" version = "0.4.13" diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 2a3bffb9..f97bb355 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -16,6 +16,8 @@ alloy.workspace = true axum.workspace = true axum-extra.workspace = true headers.workspace = true +tonic = { version = "0.12.3", features = ["tls", "channel", "prost"] } +prost = "0.13.4" # async / threads tokio.workspace = true @@ -37,14 +39,3 @@ uuid.workspace = true bimap.workspace = true lazy_static.workspace = true derive_more.workspace = true - -tonic = { version = "0.12.3", features = [ - "tls", - "channel", - "prost", - "codegen", -] } -prost = "0.13.4" - -[build-dependencies] -tonic-build = "0.12.3" From abde8b789cec742af659f13da65347913fb09c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 14:47:09 -0300 Subject: [PATCH 019/104] Moved tonic to root Cargo.toml --- Cargo.toml | 2 ++ crates/common/Cargo.toml | 8 +------- crates/signer/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b09f73f6..4c662e5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,8 @@ axum = { version = "0.7.5", features = ["macros"] } axum-extra = { version = "0.9.3", features = ["typed-header"] } reqwest = { version = "0.12.4", features = ["json", "stream"] } headers = "0.4.0" +tonic = { version = "0.12.3", features = ["tls", "channel", "prost"] } + # async / threads tokio = { version = "1.37.0", features = ["full"] } diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index ce64b7da..a388eaa3 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -14,6 +14,7 @@ ethereum_serde_utils.workspace = true # networking axum.workspace = true reqwest.workspace = true +tonic.workspace = true # async / threads tokio.workspace = true @@ -51,10 +52,3 @@ derive_more.workspace = true unicode-normalization.workspace = true base64.workspace = true - -tonic = { version = "0.12.3", features = [ - "tls", - "channel", - "prost", - "codegen", -] } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index f97bb355..63052751 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -16,7 +16,7 @@ alloy.workspace = true axum.workspace = true axum-extra.workspace = true headers.workspace = true -tonic = { version = "0.12.3", features = ["tls", "channel", "prost"] } +tonic.workspace = true prost = "0.13.4" # async / threads From 7b99746b1599da140956d68bef3ff76144ae651e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 15:05:37 -0300 Subject: [PATCH 020/104] Minor fixes --- crates/common/src/config/signer.rs | 2 +- crates/common/src/signer/types.rs | 2 +- crates/signer/src/lib.rs | 2 +- crates/signer/src/manager/local.rs | 3 +-- crates/signer/src/manager/mod.rs | 3 ++- crates/signer/src/service.rs | 4 ++-- docs/docs/get_started/configuration.md | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 4204e3f7..2df43edc 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -1,11 +1,11 @@ use std::path::PathBuf; -use alloy::transports::http::reqwest::Url; use bimap::BiHashMap; use eyre::{bail, Result}; use serde::{Deserialize, Serialize}; use tonic::transport::{Certificate, Identity}; use tracing::info; +use url::Url; use super::{ constants::SIGNER_IMAGE_DEFAULT, diff --git a/crates/common/src/signer/types.rs b/crates/common/src/signer/types.rs index fdb2c11f..4071f858 100644 --- a/crates/common/src/signer/types.rs +++ b/crates/common/src/signer/types.rs @@ -34,7 +34,7 @@ pub struct EcdsaProxySigner { pub delegation: SignedProxyDelegationEcdsa, } -#[derive(Clone, Default)] +#[derive(Default)] pub struct ProxySigners { pub bls_signers: HashMap, pub ecdsa_signers: HashMap, diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 85b32f47..4b5e1451 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -2,5 +2,5 @@ mod constants; pub mod error; pub mod manager; mod metrics; -pub mod proto; +mod proto; pub mod service; diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index 755bcb3c..353221c9 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -17,9 +17,8 @@ use tree_hash::TreeHash; use crate::error::SignerModuleError; -#[derive(Clone)] pub struct LocalSigningManager { - pub chain: Chain, + chain: Chain, proxy_store: Option, consensus_signers: HashMap, proxy_signers: ProxySigners, diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index c62920fa..96946834 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -29,7 +29,8 @@ impl SigningManager { pub async fn available_proxy_signers(&self) -> eyre::Result { match self { SigningManager::Local(local_manager) => { - let proxies = local_manager.read().await.proxies().clone(); + let manager = local_manager.read().await; + let proxies = manager.proxies(); Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) } SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.proxies().await?.len()), diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index fcb47678..ad5b5fff 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -58,10 +58,10 @@ impl SigningService { let module_ids: Vec = config.jwts.left_values().cloned().map(Into::into).collect(); - let state = match &config.dirk { + let state = match config.dirk { Some(dirk) => SigningState { manager: SigningManager::Dirk( - DirkManager::new_from_config(config.chain, dirk.clone()).await?, + DirkManager::new_from_config(config.chain, dirk).await?, ), jwts: config.jwts.into(), }, diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 8d4357d8..bab7d4dc 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -252,8 +252,8 @@ wallets = ["wallet1", "wallet2"] # Optional parameters docker_image = "ghcr.io/commit-boost/signer:latest" -ca_cert_path = "/path/to/ca.crt", -server_domain = "server.example.com", +ca_cert_path = "/path/to/ca.crt" +server_domain = "server.example.com" ``` - `cert_path` and `key_path` are the paths to the client certificate and key used to authenticate with Dirk. From 01dd5c3364f270b0070f4fe4a6945b4e7d266db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 16:33:37 -0300 Subject: [PATCH 021/104] Add the ability to unlock if sign fails --- crates/common/src/config/signer.rs | 6 +++ crates/signer/src/manager/dirk.rs | 65 ++++++++++++++++++++++++------ 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 2df43edc..1b36bb61 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -52,6 +52,9 @@ pub enum SignerConfig { ca_cert_path: Option, /// Domain name of the server to use in TLS verification server_domain: Option, + /// Whether to unlock the accounts in case they are locked + #[serde(default)] + unlock: bool, }, } @@ -66,6 +69,7 @@ pub struct DirkConfig { pub client_cert: Identity, pub cert_auth: Option, pub server_domain: Option, + pub unlock: bool, } #[derive(Debug)] @@ -102,6 +106,7 @@ impl StartSignerConfig { wallets, ca_cert_path, server_domain, + unlock, .. }) => { let cert_path = @@ -131,6 +136,7 @@ impl StartSignerConfig { None => None, }, server_domain, + unlock, }), }) } diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 7b6f3807..92b6f6ca 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -8,6 +8,7 @@ use cb_common::{ types::{Chain, ModuleId}, }; use tonic::transport::{Channel, ClientTlsConfig}; +use tracing::info; use crate::proto::v1::{ account_manager_client::AccountManagerClient, lister_client::ListerClient, @@ -20,6 +21,7 @@ pub struct DirkManager { chain: Chain, channel: Channel, wallets: Vec, + unlock: bool, } impl DirkManager { @@ -42,7 +44,7 @@ impl DirkManager { .await .map_err(|e| eyre::eyre!("Couldn't connect to Dirk: {e}"))?; - Ok(Self { chain, channel, wallets: config.wallets }) + Ok(Self { chain, channel, wallets: config.wallets, unlock: config.unlock }) } async fn get_all_accounts(&self) -> eyre::Result> { @@ -58,7 +60,7 @@ impl DirkManager { Ok(pubkeys_response.into_inner().accounts) } - async fn get_pubkey_wallet(&self, pubkey: BlsPublicKey) -> eyre::Result> { + async fn get_pubkey_account(&self, pubkey: BlsPublicKey) -> eyre::Result> { let accounts = self.get_all_accounts().await?; for account in accounts { @@ -70,6 +72,18 @@ impl DirkManager { Ok(None) } + async fn get_pubkey_wallet(&self, pubkey: BlsPublicKey) -> eyre::Result> { + let account = self.get_pubkey_account(pubkey).await?; + + if let Some(account) = account { + Ok(Some( + account.split_once("/").ok_or(eyre::eyre!("Invalid account name"))?.0.to_string(), + )) + } else { + Ok(None) + } + } + pub async fn consensus_pubkeys(&self) -> eyre::Result> { let accounts = self.get_all_accounts().await?; @@ -145,6 +159,21 @@ impl DirkManager { Ok(proxy_maps) } + async fn unlock_account(&self, account: String, password: String) -> eyre::Result<()> { + let mut client = AccountManagerClient::new(self.channel.clone()); + let unlock_request = tonic::Request::new(UnlockAccountRequest { + account, + passphrase: password.as_bytes().to_vec(), + }); + + let unlock_response = client.unlock(unlock_request).await?; + if unlock_response.get_ref().state() != ResponseState::Succeeded { + eyre::bail!("Unlock request failed"); + } + + Ok(()) + } + pub async fn generate_proxy_key( &self, module_id: ModuleId, @@ -173,16 +202,7 @@ impl DirkManager { let proxy_key = BlsPublicKey::from(FixedBytes::from_slice(&generate_response.into_inner().public_key)); - let mut unlock_client = AccountManagerClient::new(self.channel.clone()); - let unlock_request = tonic::Request::new(UnlockAccountRequest { - account: format!("{wallet}/{module_id}/{uuid}"), - passphrase: vec![0x73, 0x65, 0x63, 0x72, 0x65, 0x74], // "secret" - }); - - let unlock_response = unlock_client.unlock(unlock_request).await?; - if unlock_response.get_ref().state() != ResponseState::Succeeded { - eyre::bail!("Unlock request failed"); - } + self.unlock_account(format!("{wallet}/{module_id}/{uuid}"), "secret".to_string()).await?; Ok(SignedProxyDelegation { message: ProxyDelegation { delegator: consensus_pubkey, proxy: proxy_key }, @@ -205,6 +225,27 @@ impl DirkManager { }); let sign_response = signer_client.sign(sign_request).await?; + + let sign_response = + if sign_response.get_ref().state() == ResponseState::Denied && self.unlock { + info!("Account {pubkey:#} may be locked, unlocking and retrying..."); + + let account_name = self + .get_pubkey_account(pubkey) + .await? + .ok_or(eyre::eyre!("Public key not found"))?; + self.unlock_account(account_name, "secret".to_string()).await?; + + let sign_request = tonic::Request::new(SignRequest { + id: Some(SignerId::PublicKey(pubkey.to_vec())), + domain: domain.to_vec(), + data: object_root.to_vec(), + }); + signer_client.sign(sign_request).await? + } else { + sign_response + }; + if sign_response.get_ref().state() != ResponseState::Succeeded { eyre::bail!("Sign request failed"); } From d40e9fc08d09df286610365899ed7ea9637ba237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 17:27:22 -0300 Subject: [PATCH 022/104] Save passwords for proxy and consensus keys in files --- Cargo.lock | 1 + crates/cli/src/docker_init.rs | 26 ++++++----- crates/common/src/config/constants.rs | 3 ++ crates/common/src/config/signer.rs | 13 +++++- crates/signer/Cargo.toml | 1 + crates/signer/src/manager/dirk.rs | 62 ++++++++++++++++++++++----- 6 files changed, 83 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b2d175a..9dcc49d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1394,6 +1394,7 @@ dependencies = [ "lazy_static", "prometheus", "prost", + "rand", "thiserror 1.0.69", "tokio", "tonic", diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 31b3fb13..dd5228a2 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -6,16 +6,7 @@ use std::{ use cb_common::{ config::{ - CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, BUILDER_PORT_ENV, - BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, - LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, - PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, - PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, - SIGNER_DIRK_CA_CERT_DEFAULT, SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CERT_DEFAULT, - SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_KEY_DEFAULT, SIGNER_DIRK_KEY_ENV, - SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, - SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV, - SIGNER_URL_ENV, + CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIRK_CA_CERT_DEFAULT, SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CERT_DEFAULT, SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_DIR_SECRETS_DEFAULT, SIGNER_DIRK_DIR_SECRETS_ENV, SIGNER_DIRK_KEY_DEFAULT, SIGNER_DIRK_KEY_ENV, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_URL_ENV }, pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, @@ -441,7 +432,14 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu services.insert("cb_signer".to_owned(), Some(signer_service)); } } - Some(SignerConfig::Dirk { docker_image, cert_path, key_path, ca_cert_path, .. }) => { + Some(SignerConfig::Dirk { + docker_image, + cert_path, + key_path, + secrets_path, + ca_cert_path, + .. + }) => { if needs_signer_module { if metrics_enabled { targets.push(PrometheusTargetConfig { @@ -456,6 +454,7 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu get_env_uval(SIGNER_PORT_ENV, signer_port as u64), get_env_val(SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_CERT_DEFAULT), get_env_val(SIGNER_DIRK_KEY_ENV, SIGNER_DIRK_KEY_DEFAULT), + get_env_val(SIGNER_DIRK_DIR_SECRETS_ENV, SIGNER_DIRK_DIR_SECRETS_DEFAULT), ]); if let Some((key, val)) = chain_spec_env.clone() { @@ -486,6 +485,11 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu key_path.display(), SIGNER_DIRK_KEY_DEFAULT )), + Volumes::Simple(format!( + "{}:{}", + secrets_path.display(), + SIGNER_DIRK_DIR_SECRETS_DEFAULT + )), ]; volumes.extend(chain_spec_volume.clone()); volumes.extend(get_log_volume(&cb_config.logs, SIGNER_MODULE_NAME)); diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index 05f85aae..7955c51f 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -54,6 +54,9 @@ pub const SIGNER_DIRK_KEY_ENV: &str = "CB_SIGNER_DIRK_KEY_FILE"; pub const SIGNER_DIRK_KEY_DEFAULT: &str = "/certificates/dirk.key"; pub const SIGNER_DIRK_CA_CERT_ENV: &str = "CB_SIGNER_DIRK_CA_CERT_FILE"; pub const SIGNER_DIRK_CA_CERT_DEFAULT: &str = "/certificates/ca.crt"; +/// Path to Dirk `secrets` folder +pub const SIGNER_DIRK_DIR_SECRETS_ENV: &str = "CB_SIGNER_DIRK_SECRETS_DIR"; +pub const SIGNER_DIRK_DIR_SECRETS_DEFAULT: &str = "/dirk_secrets"; /// Path to store proxies with plaintext keys (testing only) pub const PROXY_DIR_ENV: &str = "CB_PROXY_STORE_DIR"; pub const PROXY_DIR_DEFAULT: &str = "/proxies"; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 1b36bb61..55e070a3 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -13,7 +13,10 @@ use super::{ CommitBoostConfig, SIGNER_PORT_ENV, }; use crate::{ - config::{SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_KEY_ENV}, + config::{ + SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_DIR_SECRETS_ENV, + SIGNER_DIRK_KEY_ENV, + }, signer::{ProxyStore, SignerLoader}, types::{Chain, Jwt, ModuleId}, }; @@ -48,6 +51,8 @@ pub enum SignerConfig { key_path: PathBuf, /// Wallets to use. Each wallet should have a `wallet/consensus` account wallets: Vec, + /// Path to where the account passwords are stored + secrets_path: PathBuf, /// Path to the CA certificate ca_cert_path: Option, /// Domain name of the server to use in TLS verification @@ -67,6 +72,7 @@ pub struct DirkConfig { pub url: Url, pub wallets: Vec, pub client_cert: Identity, + pub secrets_path: PathBuf, pub cert_auth: Option, pub server_domain: Option, pub unlock: bool, @@ -104,6 +110,7 @@ impl StartSignerConfig { cert_path, key_path, wallets, + secrets_path, ca_cert_path, server_domain, unlock, @@ -113,6 +120,9 @@ impl StartSignerConfig { load_env_var(SIGNER_DIRK_CERT_ENV).map(PathBuf::from).unwrap_or(cert_path); let key_path = load_env_var(SIGNER_DIRK_KEY_ENV).map(PathBuf::from).unwrap_or(key_path); + let secrets_path = load_env_var(SIGNER_DIRK_DIR_SECRETS_ENV) + .map(PathBuf::from) + .unwrap_or(secrets_path); let ca_cert_path = load_env_var(SIGNER_DIRK_CA_CERT_ENV).map(PathBuf::from).ok().or(ca_cert_path); @@ -129,6 +139,7 @@ impl StartSignerConfig { std::fs::read_to_string(cert_path)?, std::fs::read_to_string(key_path)?, ), + secrets_path, cert_auth: match ca_cert_path { Some(path) => { Some(Certificate::from_pem(std::fs::read_to_string(path)?)) diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 63052751..edb99f7b 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -39,3 +39,4 @@ uuid.workspace = true bimap.workspace = true lazy_static.workspace = true derive_more.workspace = true +rand.workspace = true diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 92b6f6ca..d14bfc5a 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -1,4 +1,6 @@ -use alloy::primitives::FixedBytes; +use std::{fs, path::PathBuf}; + +use alloy::{hex, primitives::FixedBytes}; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, config::DirkConfig, @@ -7,6 +9,7 @@ use cb_common::{ signer::{BlsPublicKey, BlsSignature}, types::{Chain, ModuleId}, }; +use rand::Rng; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::info; @@ -22,6 +25,7 @@ pub struct DirkManager { channel: Channel, wallets: Vec, unlock: bool, + secrets_path: PathBuf, } impl DirkManager { @@ -44,7 +48,13 @@ impl DirkManager { .await .map_err(|e| eyre::eyre!("Couldn't connect to Dirk: {e}"))?; - Ok(Self { chain, channel, wallets: config.wallets, unlock: config.unlock }) + Ok(Self { + chain, + channel, + wallets: config.wallets, + unlock: config.unlock, + secrets_path: config.secrets_path, + }) } async fn get_all_accounts(&self) -> eyre::Result> { @@ -159,6 +169,29 @@ impl DirkManager { Ok(proxy_maps) } + /// Generate a random password of 64 hex-characters + fn random_password() -> String { + let password_bytes: [u8; 32] = rand::thread_rng().gen(); + hex::encode(password_bytes) + } + + /// Read the password for an account from a file + fn read_password(&self, account: String) -> eyre::Result { + fs::read_to_string(self.secrets_path.join(account)) + .map_err(|e| eyre::eyre!("Couldn't read password: {e}")) + } + + /// Store the password for an account in a file + fn store_password(&self, account: String, password: String) -> eyre::Result<()> { + fs::create_dir_all( + self.secrets_path + .join(account.rsplit_once("/").ok_or(eyre::eyre!("Invalid account name"))?.0), + ) + .map_err(|e| eyre::eyre!("Couldn't write password: {e}"))?; + fs::write(self.secrets_path.join(account), password) + .map_err(|e| eyre::eyre!("Couldn't write password: {e}")) + } + async fn unlock_account(&self, account: String, password: String) -> eyre::Result<()> { let mut client = AccountManagerClient::new(self.channel.clone()); let unlock_request = tonic::Request::new(UnlockAccountRequest { @@ -186,10 +219,13 @@ impl DirkManager { .await? .ok_or(eyre::eyre!("Consensus public key not found"))?; + let account_name = format!("{wallet}/{module_id}/{uuid}"); + let new_password = Self::random_password(); + let mut client = AccountManagerClient::new(self.channel.clone()); let generate_request = tonic::Request::new(GenerateRequest { - account: format!("{wallet}/{module_id}/{uuid}"), - passphrase: vec![0x73, 0x65, 0x63, 0x72, 0x65, 0x74], // "secret" + account: account_name.clone(), + passphrase: new_password.as_bytes().to_vec(), participants: 1, signing_threshold: 1, }); @@ -199,10 +235,12 @@ impl DirkManager { eyre::bail!("Generate request failed"); } + self.store_password(account_name.clone(), new_password.clone())?; + let proxy_key = BlsPublicKey::from(FixedBytes::from_slice(&generate_response.into_inner().public_key)); - self.unlock_account(format!("{wallet}/{module_id}/{uuid}"), "secret".to_string()).await?; + self.unlock_account(account_name, new_password).await?; Ok(SignedProxyDelegation { message: ProxyDelegation { delegator: consensus_pubkey, proxy: proxy_key }, @@ -226,15 +264,17 @@ impl DirkManager { let sign_response = signer_client.sign(sign_request).await?; - let sign_response = - if sign_response.get_ref().state() == ResponseState::Denied && self.unlock { + // Retry if unlock config is set + let sign_response = match sign_response.get_ref().state() { + ResponseState::Denied if self.unlock => { info!("Account {pubkey:#} may be locked, unlocking and retrying..."); let account_name = self .get_pubkey_account(pubkey) .await? .ok_or(eyre::eyre!("Public key not found"))?; - self.unlock_account(account_name, "secret".to_string()).await?; + self.unlock_account(account_name.clone(), self.read_password(account_name)?) + .await?; let sign_request = tonic::Request::new(SignRequest { id: Some(SignerId::PublicKey(pubkey.to_vec())), @@ -242,9 +282,9 @@ impl DirkManager { data: object_root.to_vec(), }); signer_client.sign(sign_request).await? - } else { - sign_response - }; + } + _ => sign_response, + }; if sign_response.get_ref().state() != ResponseState::Succeeded { eyre::bail!("Sign request failed"); From 63d0f664385230c668349e617bd07d692bec08b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 17:31:32 -0300 Subject: [PATCH 023/104] Rename dirk constants --- crates/cli/src/docker_init.rs | 36 +++++++++++++-------------- crates/common/src/config/constants.rs | 16 ++++++------ crates/common/src/config/signer.rs | 18 +++++--------- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index dd5228a2..df50d460 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -6,7 +6,16 @@ use std::{ use cb_common::{ config::{ - CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIRK_CA_CERT_DEFAULT, SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CERT_DEFAULT, SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_DIR_SECRETS_DEFAULT, SIGNER_DIRK_DIR_SECRETS_ENV, SIGNER_DIRK_KEY_DEFAULT, SIGNER_DIRK_KEY_ENV, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV, SIGNER_URL_ENV + CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, BUILDER_PORT_ENV, + BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, + DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, + DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, + LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, + PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, + PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, + SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, + SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, SIGNER_PORT_ENV, + SIGNER_URL_ENV, }, pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, @@ -452,9 +461,9 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu get_env_val(CONFIG_ENV, CONFIG_DEFAULT), get_env_same(JWTS_ENV), get_env_uval(SIGNER_PORT_ENV, signer_port as u64), - get_env_val(SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_CERT_DEFAULT), - get_env_val(SIGNER_DIRK_KEY_ENV, SIGNER_DIRK_KEY_DEFAULT), - get_env_val(SIGNER_DIRK_DIR_SECRETS_ENV, SIGNER_DIRK_DIR_SECRETS_DEFAULT), + get_env_val(DIRK_CERT_ENV, DIRK_CERT_DEFAULT), + get_env_val(DIRK_KEY_ENV, DIRK_KEY_DEFAULT), + get_env_val(DIRK_DIR_SECRETS_ENV, DIRK_DIR_SECRETS_DEFAULT), ]); if let Some((key, val)) = chain_spec_env.clone() { @@ -475,20 +484,12 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu // volumes let mut volumes = vec![ config_volume.clone(), - Volumes::Simple(format!( - "{}:{}:ro", - cert_path.display(), - SIGNER_DIRK_CERT_DEFAULT - )), - Volumes::Simple(format!( - "{}:{}:ro", - key_path.display(), - SIGNER_DIRK_KEY_DEFAULT - )), + Volumes::Simple(format!("{}:{}:ro", cert_path.display(), DIRK_CERT_DEFAULT)), + Volumes::Simple(format!("{}:{}:ro", key_path.display(), DIRK_KEY_DEFAULT)), Volumes::Simple(format!( "{}:{}", secrets_path.display(), - SIGNER_DIRK_DIR_SECRETS_DEFAULT + DIRK_DIR_SECRETS_DEFAULT )), ]; volumes.extend(chain_spec_volume.clone()); @@ -498,10 +499,9 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu volumes.push(Volumes::Simple(format!( "{}:{}:ro", ca_cert_path.display(), - SIGNER_DIRK_CA_CERT_DEFAULT + DIRK_CA_CERT_DEFAULT ))); - let (key, val) = - get_env_val(SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CA_CERT_DEFAULT); + let (key, val) = get_env_val(DIRK_CA_CERT_ENV, DIRK_CA_CERT_DEFAULT); signer_envs.insert(key, val); } diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index 7955c51f..31580cd8 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -48,15 +48,15 @@ pub const SIGNER_DIR_KEYS_DEFAULT: &str = "/keys"; pub const SIGNER_DIR_SECRETS_ENV: &str = "CB_SIGNER_LOADER_SECRETS_DIR"; pub const SIGNER_DIR_SECRETS_DEFAULT: &str = "/secrets"; /// Path to Dirk certificate -pub const SIGNER_DIRK_CERT_ENV: &str = "CB_SIGNER_DIRK_CERT_FILE"; -pub const SIGNER_DIRK_CERT_DEFAULT: &str = "/certificates/dirk.crt"; -pub const SIGNER_DIRK_KEY_ENV: &str = "CB_SIGNER_DIRK_KEY_FILE"; -pub const SIGNER_DIRK_KEY_DEFAULT: &str = "/certificates/dirk.key"; -pub const SIGNER_DIRK_CA_CERT_ENV: &str = "CB_SIGNER_DIRK_CA_CERT_FILE"; -pub const SIGNER_DIRK_CA_CERT_DEFAULT: &str = "/certificates/ca.crt"; +pub const DIRK_CERT_ENV: &str = "CB_SIGNER_DIRK_CERT_FILE"; +pub const DIRK_CERT_DEFAULT: &str = "/certificates/dirk.crt"; +pub const DIRK_KEY_ENV: &str = "CB_SIGNER_DIRK_KEY_FILE"; +pub const DIRK_KEY_DEFAULT: &str = "/certificates/dirk.key"; +pub const DIRK_CA_CERT_ENV: &str = "CB_SIGNER_DIRK_CA_CERT_FILE"; +pub const DIRK_CA_CERT_DEFAULT: &str = "/certificates/ca.crt"; /// Path to Dirk `secrets` folder -pub const SIGNER_DIRK_DIR_SECRETS_ENV: &str = "CB_SIGNER_DIRK_SECRETS_DIR"; -pub const SIGNER_DIRK_DIR_SECRETS_DEFAULT: &str = "/dirk_secrets"; +pub const DIRK_DIR_SECRETS_ENV: &str = "CB_SIGNER_DIRK_SECRETS_DIR"; +pub const DIRK_DIR_SECRETS_DEFAULT: &str = "/dirk_secrets"; /// Path to store proxies with plaintext keys (testing only) pub const PROXY_DIR_ENV: &str = "CB_PROXY_STORE_DIR"; pub const PROXY_DIR_DEFAULT: &str = "/proxies"; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 55e070a3..d83505d3 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -13,10 +13,7 @@ use super::{ CommitBoostConfig, SIGNER_PORT_ENV, }; use crate::{ - config::{ - SIGNER_DIRK_CA_CERT_ENV, SIGNER_DIRK_CERT_ENV, SIGNER_DIRK_DIR_SECRETS_ENV, - SIGNER_DIRK_KEY_ENV, - }, + config::{DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV}, signer::{ProxyStore, SignerLoader}, types::{Chain, Jwt, ModuleId}, }; @@ -116,15 +113,12 @@ impl StartSignerConfig { unlock, .. }) => { - let cert_path = - load_env_var(SIGNER_DIRK_CERT_ENV).map(PathBuf::from).unwrap_or(cert_path); - let key_path = - load_env_var(SIGNER_DIRK_KEY_ENV).map(PathBuf::from).unwrap_or(key_path); - let secrets_path = load_env_var(SIGNER_DIRK_DIR_SECRETS_ENV) - .map(PathBuf::from) - .unwrap_or(secrets_path); + let cert_path = load_env_var(DIRK_CERT_ENV).map(PathBuf::from).unwrap_or(cert_path); + let key_path = load_env_var(DIRK_KEY_ENV).map(PathBuf::from).unwrap_or(key_path); + let secrets_path = + load_env_var(DIRK_DIR_SECRETS_ENV).map(PathBuf::from).unwrap_or(secrets_path); let ca_cert_path = - load_env_var(SIGNER_DIRK_CA_CERT_ENV).map(PathBuf::from).ok().or(ca_cert_path); + load_env_var(DIRK_CA_CERT_ENV).map(PathBuf::from).ok().or(ca_cert_path); Ok(StartSignerConfig { chain: config.chain, From e367ea14d2b69c47867b0fb37f364963b77e1107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 17:38:53 -0300 Subject: [PATCH 024/104] Update docs --- config.example.toml | 5 +++++ docs/docs/get_started/configuration.md | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/config.example.toml b/config.example.toml index 7ec87adb..4fc3787d 100644 --- a/config.example.toml +++ b/config.example.toml @@ -150,12 +150,17 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # key_path = "/path/to/client.key" # Wallets to use. Each wallet should have a `wallet/consensus` account # wallets = ["wallet1", "wallet2"] +# Path to the secrets directory where the accounts passwords are stored +# secrets_path = "/path/to/secrets" # Path to the CA certificate that signed the Dirk server certificate # OPTIONAL # ca_cert_path = "/path/to/ca.crt" # Domain name of the server to use in TLS verification, if different from the URL # OPTIONAL # server_domain = "server.example.com" +# Whether to try to unlock the accounts on sign failure +# OPTIONAL, DEFAULT: false +# unlock = false # Local: [signer.local] # Docker image to use for the Signer module. diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index bab7d4dc..f90dab38 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -249,15 +249,19 @@ url = "https://dirk.gateway.url" cert_path = "/path/to/client.crt" key_path = "/path/to/client.key" wallets = ["wallet1", "wallet2"] +secrets_path = "/path/to/secrets" # Optional parameters docker_image = "ghcr.io/commit-boost/signer:latest" ca_cert_path = "/path/to/ca.crt" server_domain = "server.example.com" +unlock = false ``` - `cert_path` and `key_path` are the paths to the client certificate and key used to authenticate with Dirk. - `wallets` is a list of wallets that the Signer module will use to sign transactions. Each wallet should have a `/consensus` account which will be used as the consensus key. Generated proxy keys will be stored in `//`. +- `secrets_path` is the path to the folder containing the passwords of the accounts. Generated proxy accounts secrets will be stored in `///`. +- `unlock` is an optional parameter that can be set to `true` if you want to try to unlock the wallets on sign failure. Default is `false`. ## Custom module We currently provide a test module that needs to be built locally. To build the module run: From 95fea955c814f8eb57fce0bc94f9b3d35a60fb85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 18:02:03 -0300 Subject: [PATCH 025/104] Fix typo --- docs/docs/get_started/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index f90dab38..83f0c22f 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -229,7 +229,7 @@ To persist proxy keys across restarts, you must enable the proxy store in the co ### Remote signer -You might choose to use an external service to sign the transactions. For now, two type of remote signers are supported: Web3Signer and Dirk. +You might choose to use an external service to sign the transactions. For now, two types of remote signers are supported: Web3Signer and Dirk. #### Web3Signer From a54b4120035869c83f5d898d8d37434dff13a830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 6 Jan 2025 18:11:17 -0300 Subject: [PATCH 026/104] Update docs --- docs/docs/get_started/running/binary.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/docs/get_started/running/binary.md b/docs/docs/get_started/running/binary.md index 458b08de..b5c962d1 100644 --- a/docs/docs/get_started/running/binary.md +++ b/docs/docs/get_started/running/binary.md @@ -33,6 +33,12 @@ Modules need some environment variables to work correctly. - `CB_SIGNER_LOADER_FORMAT`, `CB_SIGNER_LOADER_KEYS_DIR` and `CB_SIGNER_LOADER_SECRETS_DIR`: paths to the `keys` and `secrets` directories or files (ERC-2335 style keystores, see [Signer config](../configuration/#signer-module) for more info). - For storing proxy keys we currently support: - `CB_PROXY_STORE_DIR`: directory where proxy keys and delegations will be saved in plaintext (for testing purposes only). + - `CB_PROXY_KEYS_DIR` and `CB_PROXY_SECRETS_DIR`: paths to the `keys` and `secrets` directories or files (ERC-2335 style keystores, see [Proxy keys store](../configuration/#proxy-keys-store) for more info). +- For Dirk remote signer the following envs are available (see [Dirk config](../configuration/#dirk) for more info): + - `CB_SIGNER_DIRK_CERT_FILE`: required, path to the client certificate file. + - `CB_SIGNER_DIRK_KEY_FILE`: required, path to the client key file. + - `CB_SIGNER_DIRK_SECRETS_DIR`: required, path to the secrets directory. + - `CB_SIGNER_DIRK_CA_CERT_FILE`: optional, path to the CA certificate file. ### Modules - `CB_MODULE_ID`: required, unique id of the module. From 2860424000a18e16d9cc02ec7e14e0836be9aee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 9 Jan 2025 13:28:49 -0300 Subject: [PATCH 027/104] Add full Dirk config example --- examples/configs/dirk_signer.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 examples/configs/dirk_signer.toml diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml new file mode 100644 index 00000000..76698e00 --- /dev/null +++ b/examples/configs/dirk_signer.toml @@ -0,0 +1,23 @@ +chain = "Holesky" + +[pbs] +port = 18550 + +[[relays]] +url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09fe73ccd21f88eab31d6de16194d17782e@def.xyz" + +[signer.dirk] +docker_image = "commitboost_signer" +url = "https://gateway.dirk.url" +cert_path = "/path/to/client.crt" +key_path = "/path/to/client.key" +wallets = ["validator1", "validator2"] +secrets_path = "./dirk_secrets" +unlock = true + +[[modules]] +id = "DA_COMMIT" +type = "commit" +docker_image = "test_da_commit" +sleep_secs = 10 +use_ecdsa_keys = false From 49993028141ae3b84efa5b28115293950899756c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 9 Jan 2025 13:38:42 -0300 Subject: [PATCH 028/104] Move docker_init to [signer] --- config.example.toml | 20 +++++------ crates/cli/src/docker_init.rs | 44 ++++++++++++----------- crates/common/src/config/signer.rs | 49 +++++++++++++++----------- docs/docs/get_started/configuration.md | 1 - examples/configs/dirk_signer.toml | 4 ++- 5 files changed, 63 insertions(+), 55 deletions(-) diff --git a/config.example.toml b/config.example.toml index 4fc3787d..9650b6a3 100644 --- a/config.example.toml +++ b/config.example.toml @@ -131,17 +131,17 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # - Remote: a remote Web3Signer instance # - Dirk: a remote Dirk instance # - Local: a local Signer module -# More details on the docs (https://commit-boost.github.io/commit-boost-client/get_started/configuration/#local-signer) -# OPTIONAL -# Remote: +# More details on the docs (https://commit-boost.github.io/commit-boost-client/get_started/configuration/#signer-module) +# Docker image to use for the Signer module. +# OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest +# [signer] +# docker_image = "ghcr.io/commit-boost/signer:latest" +# For Remote signer: # [signer.remote] # URL of the Web3Signer instance # url = "https://remote.signer.url" -# Dirk: +# For Dirk signer: # [signer.dirk] -# Docker image to use for the Signer module. -# OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest -# docker_image = "ghcr.io/commit-boost/signer:latest" # Complete URL of a Dirk gateway # url = "https://dirk.gateway.url" # Path to the client certificate to authenticate with Dirk @@ -161,11 +161,7 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # Whether to try to unlock the accounts on sign failure # OPTIONAL, DEFAULT: false # unlock = false -# Local: -[signer.local] -# Docker image to use for the Signer module. -# OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest -docker_image = "ghcr.io/commit-boost/signer:latest" +# For Local signer: # Configuration for how the Signer module should load validator keys. Currently two types of loaders are supported: # - File: load keys from a plain text file (unsafe, use only for testing purposes) # - ValidatorsDir: load keys from a `keys` and `secrets` file/folder (ERC-2335 style keystores). More details can be found in the docs (https://commit-boost.github.io/commit-boost-client/get_started/configuration/) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index df50d460..b01cab68 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -6,7 +6,7 @@ use std::{ use cb_common::{ config::{ - CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, BUILDER_PORT_ENV, + CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, @@ -79,11 +79,12 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu // address for signer API communication let signer_port = 20000; - let signer_server = if let Some(SignerConfig::Remote { url }) = &cb_config.signer { - url.to_string() - } else { - format!("http://cb_signer:{signer_port}") - }; + let signer_server = + if let Some(SignerConfig { inner: SignerType::Remote { url }, .. }) = &cb_config.signer { + url.to_string() + } else { + format!("http://cb_signer:{signer_port}") + }; let builder_events_port = 30000; let mut builder_events_modules = Vec::new(); @@ -168,7 +169,11 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu networks: Networks::Simple(module_networks), volumes: module_volumes, environment: Environment::KvPair(module_envs), - depends_on: if let Some(SignerConfig::Remote { .. }) = &cb_config.signer { + depends_on: if let Some(SignerConfig { + inner: SignerType::Remote { .. }, + .. + }) = &cb_config.signer + { DependsOnOptions::Simple(vec![]) } else { DependsOnOptions::Conditional(module_dependencies) @@ -313,9 +318,13 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu services.insert("cb_pbs".to_owned(), Some(pbs_service)); + let Some(signer_config) = cb_config.signer else { + panic!("Signer module required but no signer config provided"); + }; + // setup signer service - match cb_config.signer { - Some(SignerConfig::Local { docker_image, loader, store }) => { + match signer_config.inner { + SignerType::Local { loader, store } => { if needs_signer_module { if metrics_enabled { targets.push(PrometheusTargetConfig { @@ -421,7 +430,7 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu let signer_service = Service { container_name: Some("cb_signer".to_owned()), - image: Some(docker_image), + image: Some(signer_config.docker_image), networks: Networks::Simple(signer_networks), volumes, environment: Environment::KvPair(signer_envs), @@ -441,14 +450,7 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu services.insert("cb_signer".to_owned(), Some(signer_service)); } } - Some(SignerConfig::Dirk { - docker_image, - cert_path, - key_path, - secrets_path, - ca_cert_path, - .. - }) => { + SignerType::Dirk { cert_path, key_path, secrets_path, ca_cert_path, .. } => { if needs_signer_module { if metrics_enabled { targets.push(PrometheusTargetConfig { @@ -513,7 +515,7 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu let signer_service = Service { container_name: Some("cb_signer".to_owned()), - image: Some(docker_image), + image: Some(signer_config.docker_image), networks: Networks::Simple(signer_networks), volumes, environment: Environment::KvPair(signer_envs), @@ -533,8 +535,8 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu services.insert("cb_signer".to_owned(), Some(signer_service)); } } - _ => { - panic!("Signer module required but no signer config provided"); + SignerType::Remote { .. } => { + panic!("Signer module required but remote config provided"); } } diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index d83505d3..573f0239 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -1,10 +1,9 @@ use std::path::PathBuf; use bimap::BiHashMap; -use eyre::{bail, Result}; +use eyre::{bail, OptionExt, Result}; use serde::{Deserialize, Serialize}; use tonic::transport::{Certificate, Identity}; -use tracing::info; use url::Url; use super::{ @@ -20,12 +19,24 @@ use crate::{ #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] -pub enum SignerConfig { +pub struct SignerConfig { + /// Docker image of the module + #[serde(default = "default_signer")] + pub docker_image: String, + /// Inner type-specific configuration + #[serde(flatten)] + pub inner: SignerType, +} + +fn default_signer() -> String { + SIGNER_IMAGE_DEFAULT.to_string() +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub enum SignerType { /// Local signer module Local { - /// Docker image of the module - #[serde(default = "default_signer")] - docker_image: String, /// Which keys to load loader: SignerLoader, /// How to store keys @@ -36,10 +47,8 @@ pub enum SignerConfig { /// Complete URL of the base API endpoint url: Url, }, + /// Dirk remote signer module Dirk { - /// Docker image of the module - #[serde(default = "default_signer")] - docker_image: String, /// Complete URL of a Dirk gateway url: Url, /// Path to the client certificate @@ -60,10 +69,6 @@ pub enum SignerConfig { }, } -fn default_signer() -> String { - SIGNER_IMAGE_DEFAULT.to_string() -} - #[derive(Clone, Debug)] pub struct DirkConfig { pub url: Url, @@ -87,14 +92,15 @@ pub struct StartSignerConfig { impl StartSignerConfig { pub fn load_from_env() -> Result { - info!("Loading from env"); let config = CommitBoostConfig::from_env_path()?; let jwts = load_jwts()?; let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?; - match config.signer { - Some(SignerConfig::Local { loader, store, .. }) => Ok(StartSignerConfig { + let signer = config.signer.ok_or_eyre("Signer config is missing")?.inner; + + match signer { + SignerType::Local { loader, store, .. } => Ok(StartSignerConfig { chain: config.chain, loader: Some(loader), server_port, @@ -102,7 +108,8 @@ impl StartSignerConfig { store, dirk: None, }), - Some(SignerConfig::Dirk { + + SignerType::Dirk { url, cert_path, key_path, @@ -112,7 +119,7 @@ impl StartSignerConfig { server_domain, unlock, .. - }) => { + } => { let cert_path = load_env_var(DIRK_CERT_ENV).map(PathBuf::from).unwrap_or(cert_path); let key_path = load_env_var(DIRK_KEY_ENV).map(PathBuf::from).unwrap_or(key_path); let secrets_path = @@ -145,8 +152,10 @@ impl StartSignerConfig { }), }) } - Some(SignerConfig::Remote { .. }) => bail!("Remote signer configured"), - None => bail!("Signer config is missing"), + + SignerType::Remote { .. } => { + bail!("Remote signer configured") + } } } } diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 83f0c22f..719cbe53 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -252,7 +252,6 @@ wallets = ["wallet1", "wallet2"] secrets_path = "/path/to/secrets" # Optional parameters -docker_image = "ghcr.io/commit-boost/signer:latest" ca_cert_path = "/path/to/ca.crt" server_domain = "server.example.com" unlock = false diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml index 76698e00..039dca3e 100644 --- a/examples/configs/dirk_signer.toml +++ b/examples/configs/dirk_signer.toml @@ -6,8 +6,10 @@ port = 18550 [[relays]] url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09fe73ccd21f88eab31d6de16194d17782e@def.xyz" -[signer.dirk] +[signer] docker_image = "commitboost_signer" + +[signer.dirk] url = "https://gateway.dirk.url" cert_path = "/path/to/client.crt" key_path = "/path/to/client.key" From cf2e03ee3b32710aa597ba838b1e3d3c1f46fcb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 9 Jan 2025 13:39:24 -0300 Subject: [PATCH 029/104] Move dependency to root Cargo.toml --- Cargo.toml | 2 +- crates/signer/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4c662e5a..8cf807e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ axum-extra = { version = "0.9.3", features = ["typed-header"] } reqwest = { version = "0.12.4", features = ["json", "stream"] } headers = "0.4.0" tonic = { version = "0.12.3", features = ["tls", "channel", "prost"] } - +prost = "0.13.4" # async / threads tokio = { version = "1.37.0", features = ["full"] } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index edb99f7b..e5a31bce 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -17,7 +17,7 @@ axum.workspace = true axum-extra.workspace = true headers.workspace = true tonic.workspace = true -prost = "0.13.4" +prost.workspace = true # async / threads tokio.workspace = true From 07c11c52f08a8226317c813b60fdd80ef7ff0b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 9 Jan 2025 13:42:31 -0300 Subject: [PATCH 030/104] Improve error message --- crates/signer/src/manager/dirk.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index d14bfc5a..f10870e9 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -87,7 +87,13 @@ impl DirkManager { if let Some(account) = account { Ok(Some( - account.split_once("/").ok_or(eyre::eyre!("Invalid account name"))?.0.to_string(), + account + .split_once("/") + .ok_or(eyre::eyre!( + "Invalid account name: {account}. It must be in format wallet/account" + ))? + .0 + .to_string(), )) } else { Ok(None) From 904d7b2f7a3ef015309a19786bb96a4548ec386c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 9 Jan 2025 13:57:51 -0300 Subject: [PATCH 031/104] Make BlsPublicKey parsing fallible --- crates/common/src/signer/schemes/bls.rs | 8 ++++++++ crates/signer/src/manager/dirk.rs | 14 ++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/common/src/signer/schemes/bls.rs b/crates/common/src/signer/schemes/bls.rs index 82d0c01a..5e9afade 100644 --- a/crates/common/src/signer/schemes/bls.rs +++ b/crates/common/src/signer/schemes/bls.rs @@ -36,6 +36,14 @@ impl AsRef<[u8]> for BlsPublicKey { } } +impl TryFrom<&[u8]> for BlsPublicKey { + type Error = core::array::TryFromSliceError; + + fn try_from(value: &[u8]) -> Result { + Ok(Self { inner: value.try_into()? }) + } +} + #[derive(Clone)] pub enum BlsSigner { Local(BlsSecretKey), diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index f10870e9..cfa6fe42 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -110,7 +110,7 @@ impl DirkManager { .iter() .filter_map(|account| { if expected_accounts.contains(&account.name) { - Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) + BlsPublicKey::try_from(account.public_key.as_slice()).ok() } else { None } @@ -128,7 +128,7 @@ impl DirkManager { if self.wallets.contains(&wallet.to_string()) && account.name != format!("{wallet}/consensus") { - Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) + BlsPublicKey::try_from(account.public_key.as_slice()).ok() } else { None } @@ -147,7 +147,7 @@ impl DirkManager { for wallet in self.wallets.iter() { let Some(consensus_key) = accounts.iter().find_map(|account| { if account.name == format!("{wallet}/consensus") { - Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) + BlsPublicKey::try_from(account.public_key.as_slice()).ok() } else { None } @@ -159,7 +159,7 @@ impl DirkManager { .iter() .filter_map(|account| { if account.name.starts_with(&format!("{wallet}/{module_id}")) { - Some(BlsPublicKey::from(FixedBytes::from_slice(&account.public_key))) + BlsPublicKey::try_from(account.public_key.as_slice()).ok() } else { None } @@ -244,7 +244,7 @@ impl DirkManager { self.store_password(account_name.clone(), new_password.clone())?; let proxy_key = - BlsPublicKey::from(FixedBytes::from_slice(&generate_response.into_inner().public_key)); + BlsPublicKey::try_from(generate_response.into_inner().public_key.as_slice())?; self.unlock_account(account_name, new_password).await?; @@ -296,6 +296,8 @@ impl DirkManager { eyre::bail!("Sign request failed"); } - Ok(BlsSignature::from(FixedBytes::from_slice(&sign_response.into_inner().signature))) + Ok(BlsSignature::from(FixedBytes::try_from( + sign_response.into_inner().signature.as_slice(), + )?)) } } From 951cbb2bc11bdee2892ea850f19f4b7996893938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 9 Jan 2025 16:02:32 -0300 Subject: [PATCH 032/104] Remove proto files --- .../src/proto/dirk/accountmanager.proto | 65 ---- crates/signer/src/proto/dirk/dkg.proto | 70 ---- crates/signer/src/proto/dirk/endpoint.proto | 16 - crates/signer/src/proto/dirk/eth2.proto | 34 -- crates/signer/src/proto/dirk/lister.proto | 47 --- .../signer/src/proto/dirk/responsestate.proto | 21 -- crates/signer/src/proto/dirk/signer.proto | 86 ----- .../signer/src/proto/dirk/walletmanager.proto | 43 --- .../third-party/google/api/annotations.proto | 31 -- .../proto/third-party/google/api/http.proto | 318 ------------------ 10 files changed, 731 deletions(-) delete mode 100644 crates/signer/src/proto/dirk/accountmanager.proto delete mode 100644 crates/signer/src/proto/dirk/dkg.proto delete mode 100644 crates/signer/src/proto/dirk/endpoint.proto delete mode 100644 crates/signer/src/proto/dirk/eth2.proto delete mode 100644 crates/signer/src/proto/dirk/lister.proto delete mode 100644 crates/signer/src/proto/dirk/responsestate.proto delete mode 100644 crates/signer/src/proto/dirk/signer.proto delete mode 100644 crates/signer/src/proto/dirk/walletmanager.proto delete mode 100644 crates/signer/src/proto/third-party/google/api/annotations.proto delete mode 100644 crates/signer/src/proto/third-party/google/api/http.proto diff --git a/crates/signer/src/proto/dirk/accountmanager.proto b/crates/signer/src/proto/dirk/accountmanager.proto deleted file mode 100644 index 6ca3abb8..00000000 --- a/crates/signer/src/proto/dirk/accountmanager.proto +++ /dev/null @@ -1,65 +0,0 @@ -syntax = "proto3"; - -package v1; - -import "google/api/annotations.proto"; -import "responsestate.proto"; -import "endpoint.proto"; - -option csharp_namespace = "Eth2Signer.v1"; -option php_namespace = "Eth2Signer\\v1"; -option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; -option java_package = "com.wealdtech.eth2signerapi.v1"; -option java_multiple_files = true; -option java_outer_classname = "AccountManagerProto"; - -service AccountManager { - rpc Unlock(UnlockAccountRequest) returns (UnlockAccountResponse) { - option (google.api.http) = { - get: "/v1/accountmanager/unlock" - }; - } - - rpc Lock(LockAccountRequest) returns (LockAccountResponse) { - option (google.api.http) = { - get: "/v1/accountmanager/lock" - }; - } - - rpc Generate(GenerateRequest) returns (GenerateResponse) { - option (google.api.http) = { - post: "/v1/accountmanager/generate" - }; - } -} - -message UnlockAccountRequest { - string account = 1; - bytes passphrase = 2; -} - -message LockAccountRequest { - string account = 1; -} - -message UnlockAccountResponse { - ResponseState state = 1; -} - -message LockAccountResponse { - ResponseState state = 1; -} - -message GenerateRequest { - string account = 1; - bytes passphrase = 2; - uint32 participants = 3; - uint32 signing_threshold = 4; -} - -message GenerateResponse { - ResponseState state = 1; - string message = 2; - bytes public_key = 3; - repeated Endpoint participants = 4; -} diff --git a/crates/signer/src/proto/dirk/dkg.proto b/crates/signer/src/proto/dirk/dkg.proto deleted file mode 100644 index e335a4b7..00000000 --- a/crates/signer/src/proto/dirk/dkg.proto +++ /dev/null @@ -1,70 +0,0 @@ -syntax = "proto3"; - -package v1; - -import "google/protobuf/empty.proto"; -import "endpoint.proto"; - -option csharp_namespace = "Eth2Signer.v1"; -option php_namespace = "Eth2Signer\\v1"; -option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; -option java_package = "com.wealdtech.eth2signerapi.v1"; -option java_multiple_files = true; -option java_outer_classname = "DKGProto"; - -// DKG is the internal protocol that runs between distributed key generators. -service DKG { - rpc Prepare(PrepareRequest) returns (google.protobuf.Empty) { } - rpc Execute(ExecuteRequest) returns (google.protobuf.Empty) { } - rpc Commit(CommitRequest) returns (CommitResponse) { } - rpc Abort(AbortRequest) returns (google.protobuf.Empty) { } - rpc Contribute(ContributeRequest) returns (ContributeResponse) { } -} - -message PrepareRequest { - // account is the name of the account. - string account = 1; - // threshold is the number of participants required to generate a valid signature. - uint32 threshold = 2; - // participants contains the endpoints of all participants. - repeated Endpoint participants = 3; - // passphrase is the passphrase of the account. - bytes passphrase = 4; -} - -message ExecuteRequest { - // account is the name of the account. - string account = 1; -} - -message CommitRequest { - // account is the name of the account. - string account = 1; - // confirmation data is data used to generate the confirmation signature. - bytes confirmation_data = 2; -} - -message CommitResponse { - // public_key is the key generated by the process. - bytes public_key = 1; - // confirmation_signature is the signature generated by the individual secret key. - bytes confirmation_signature = 2; -} - -message AbortRequest { - // account is the name of the account. - string account = 1; -} - -// ContributeRequest is sent by each part to all other parties with a contribution. -message ContributeRequest { - string account = 1; - bytes secret = 2; - repeated bytes verification_vector = 3; -} - -// ContributeResponse receives the contribution from a participant. -message ContributeResponse { - bytes secret = 1; - repeated bytes verification_vector = 2; -} diff --git a/crates/signer/src/proto/dirk/endpoint.proto b/crates/signer/src/proto/dirk/endpoint.proto deleted file mode 100644 index e61f84ae..00000000 --- a/crates/signer/src/proto/dirk/endpoint.proto +++ /dev/null @@ -1,16 +0,0 @@ -syntax = "proto3"; - -package v1; - -option csharp_namespace = "Eth2Signer.v1"; -option php_namespace = "Eth2Signer\\v1"; -option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; -option java_package = "com.wealdtech.eth2signerapi.v1"; -option java_multiple_files = true; -option java_outer_classname = "EndpointProto"; - -message Endpoint { - uint64 id = 1; - string name = 2; - uint32 port = 3; -} diff --git a/crates/signer/src/proto/dirk/eth2.proto b/crates/signer/src/proto/dirk/eth2.proto deleted file mode 100644 index 615ba814..00000000 --- a/crates/signer/src/proto/dirk/eth2.proto +++ /dev/null @@ -1,34 +0,0 @@ -syntax = "proto3"; - -package v1; - -option csharp_namespace = "Eth2Signer.v1"; -option php_namespace = "Eth2Signer\\v1"; -option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; -option java_package = "com.wealdtech.eth2signerapi.v1"; -option java_multiple_files = true; -option java_outer_classname = "Eth2Proto"; - -// AttestationData is defined at https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md#attestationdata -message AttestationData { - uint64 slot = 1; - uint64 committee_index = 2; - bytes beacon_block_root = 3; - Checkpoint source = 4; - Checkpoint target = 5; -} - -// Checkpoint is defined at https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md#checkpoint -message Checkpoint { - uint64 epoch = 1; - bytes root = 2; -} - -// BeaconBlockheader is defined at https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader -message BeaconBlockHeader { - uint64 slot = 1; - uint64 proposer_index = 2; - bytes parent_root = 3; - bytes state_root = 4; - bytes body_root = 5; -} diff --git a/crates/signer/src/proto/dirk/lister.proto b/crates/signer/src/proto/dirk/lister.proto deleted file mode 100644 index b3d6642b..00000000 --- a/crates/signer/src/proto/dirk/lister.proto +++ /dev/null @@ -1,47 +0,0 @@ -syntax = "proto3"; - -package v1; - -import "google/api/annotations.proto"; -import "endpoint.proto"; -import "responsestate.proto"; - -option csharp_namespace = "Eth2Signer.v1"; -option php_namespace = "Eth2Signer\\v1"; -option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; -option java_package = "com.wealdtech.eth2signerapi.v1"; -option java_multiple_files = true; -option java_outer_classname = "ListerProto"; - -service Lister { - rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) { - option (google.api.http) = { - get: "/v1/lister/listaccounts" - }; - } -} - -message ListAccountsRequest { - repeated string paths = 1; -} - -message ListAccountsResponse { - ResponseState state = 1; - repeated Account Accounts = 2; - repeated DistributedAccount DistributedAccounts = 3; -} - -message Account { - string name = 1; - bytes public_key = 2; - bytes uuid = 3; -} - -message DistributedAccount { - string name = 1; - bytes public_key = 2; - repeated Endpoint participants = 3; - uint32 signing_threshold = 4; - bytes uuid = 5; - bytes composite_public_key = 6; -} diff --git a/crates/signer/src/proto/dirk/responsestate.proto b/crates/signer/src/proto/dirk/responsestate.proto deleted file mode 100644 index 8eb57d9e..00000000 --- a/crates/signer/src/proto/dirk/responsestate.proto +++ /dev/null @@ -1,21 +0,0 @@ -syntax = "proto3"; - -package v1; - -option csharp_namespace = "Eth2Signer.v1"; -option php_namespace = "Eth2Signer\\v1"; -option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; -option java_package = "com.wealdtech.eth2signerapi.v1"; -option java_multiple_files = true; -option java_outer_classname = "ResponseStateProto"; - -enum ResponseState { - // UNKNOWN occurs when no information about the response is available. - UNKNOWN = 0; - // SUCCEEDED occurs when a request was successful. - SUCCEEDED = 1; - // DENIED occurs when a request was denied. - DENIED = 2; - // FAILED occurs when a request failed to complete. - FAILED = 3; -} diff --git a/crates/signer/src/proto/dirk/signer.proto b/crates/signer/src/proto/dirk/signer.proto deleted file mode 100644 index 02612063..00000000 --- a/crates/signer/src/proto/dirk/signer.proto +++ /dev/null @@ -1,86 +0,0 @@ -syntax = "proto3"; - -package v1; - -import "google/api/annotations.proto"; -import "eth2.proto"; -import "responsestate.proto"; - -option csharp_namespace = "Eth2Signer.v1"; -option php_namespace = "Eth2Signer\\v1"; -option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; -option java_package = "com.wealdtech.eth2signerapi.v1"; -option java_multiple_files = true; -option java_outer_classname = "SignerProto"; - -service Signer { - rpc Sign(SignRequest) returns (SignResponse) { - option (google.api.http) = { - get: "/v1/signer/sign" - }; - } - rpc Multisign(MultisignRequest) returns (MultisignResponse) { - option (google.api.http) = { - get: "/v1/signer/multisign" - }; - } - rpc SignBeaconAttestation(SignBeaconAttestationRequest) returns (SignResponse) { - option (google.api.http) = { - get: "/v1/signer/signbeaconattestation" - }; - } - rpc SignBeaconAttestations(SignBeaconAttestationsRequest) returns (MultisignResponse) { - option (google.api.http) = { - get: "/v1/signer/signbeaconattestations" - }; - } - rpc SignBeaconProposal(SignBeaconProposalRequest) returns (SignResponse) { - option (google.api.http) = { - get: "/v1/signer/signbeaconproposal" - }; - } -} - -message SignRequest { - oneof id { - bytes public_key = 1; - string account = 2; - } - bytes data = 3; - bytes domain = 4; -} - -message MultisignRequest { - repeated SignRequest requests = 1; -} - -message SignBeaconAttestationRequest { - oneof id { - bytes public_key = 1; - string account = 2; - } - bytes domain = 3; - AttestationData data = 4; -} - -message SignBeaconAttestationsRequest { - repeated SignBeaconAttestationRequest requests = 1; -} - -message SignBeaconProposalRequest { - oneof id { - bytes public_key = 1; - string account = 2; - } - bytes domain = 3; - BeaconBlockHeader data = 4; -} - -message SignResponse { - ResponseState state = 1; - bytes signature = 2; -} - -message MultisignResponse { - repeated SignResponse responses = 1; -} diff --git a/crates/signer/src/proto/dirk/walletmanager.proto b/crates/signer/src/proto/dirk/walletmanager.proto deleted file mode 100644 index 98a5d8a0..00000000 --- a/crates/signer/src/proto/dirk/walletmanager.proto +++ /dev/null @@ -1,43 +0,0 @@ -syntax = "proto3"; - -package v1; - -import "google/api/annotations.proto"; -import "responsestate.proto"; - -option csharp_namespace = "Eth2Signer.v1"; -option php_namespace = "Eth2Signer\\v1"; -option java_package = "com.wealdtech.eth2signerapi.v1"; -option go_package = "github.com/wealdtech/eth2-signer-api/pb/v1"; -option java_multiple_files = true; -option java_outer_classname = "WalletManagerProto"; - -service WalletManager { - rpc Unlock(UnlockWalletRequest) returns (UnlockWalletResponse) { - option (google.api.http) = { - get: "/v1/walletmanager/unlock" - }; - } - rpc Lock(LockWalletRequest) returns (LockWalletResponse) { - option (google.api.http) = { - get: "/v1/walletmanager/lock" - }; - } -} - -message UnlockWalletRequest { - string wallet = 1; - bytes passphrase = 2; -} - -message LockWalletRequest { - string wallet = 1; -} - -message UnlockWalletResponse { - ResponseState state = 1; -} - -message LockWalletResponse { - ResponseState state = 1; -} diff --git a/crates/signer/src/proto/third-party/google/api/annotations.proto b/crates/signer/src/proto/third-party/google/api/annotations.proto deleted file mode 100644 index 85c361b4..00000000 --- a/crates/signer/src/proto/third-party/google/api/annotations.proto +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2015, Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -import "google/api/http.proto"; -import "google/protobuf/descriptor.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "AnnotationsProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -extend google.protobuf.MethodOptions { - // See `HttpRule`. - HttpRule http = 72295728; -} diff --git a/crates/signer/src/proto/third-party/google/api/http.proto b/crates/signer/src/proto/third-party/google/api/http.proto deleted file mode 100644 index 2bd3a19b..00000000 --- a/crates/signer/src/proto/third-party/google/api/http.proto +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "HttpProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - - -// Defines the HTTP configuration for an API service. It contains a list of -// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method -// to one or more HTTP REST API methods. -message Http { - // A list of HTTP configuration rules that apply to individual API methods. - // - // **NOTE:** All service configuration rules follow "last one wins" order. - repeated HttpRule rules = 1; - - // When set to true, URL path parmeters will be fully URI-decoded except in - // cases of single segment matches in reserved expansion, where "%2F" will be - // left encoded. - // - // The default behavior is to not decode RFC 6570 reserved characters in multi - // segment matches. - bool fully_decode_reserved_expansion = 2; -} - -// `HttpRule` defines the mapping of an RPC method to one or more HTTP -// REST API methods. The mapping specifies how different portions of the RPC -// request message are mapped to URL path, URL query parameters, and -// HTTP request body. The mapping is typically specified as an -// `google.api.http` annotation on the RPC method, -// see "google/api/annotations.proto" for details. -// -// The mapping consists of a field specifying the path template and -// method kind. The path template can refer to fields in the request -// message, as in the example below which describes a REST GET -// operation on a resource collection of messages: -// -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}"; -// } -// } -// message GetMessageRequest { -// message SubMessage { -// string subfield = 1; -// } -// string message_id = 1; // mapped to the URL -// SubMessage sub = 2; // `sub.subfield` is url-mapped -// } -// message Message { -// string text = 1; // content of the resource -// } -// -// The same http annotation can alternatively be expressed inside the -// `GRPC API Configuration` YAML file. -// -// http: -// rules: -// - selector: .Messaging.GetMessage -// get: /v1/messages/{message_id}/{sub.subfield} -// -// This definition enables an automatic, bidrectional mapping of HTTP -// JSON to RPC. Example: -// -// HTTP | RPC -// -----|----- -// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))` -// -// In general, not only fields but also field paths can be referenced -// from a path pattern. Fields mapped to the path pattern cannot be -// repeated and must have a primitive (non-message) type. -// -// Any fields in the request message which are not bound by the path -// pattern automatically become (optional) HTTP query -// parameters. Assume the following definition of the request message: -// -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http).get = "/v1/messages/{message_id}"; -// } -// } -// message GetMessageRequest { -// message SubMessage { -// string subfield = 1; -// } -// string message_id = 1; // mapped to the URL -// int64 revision = 2; // becomes a parameter -// SubMessage sub = 3; // `sub.subfield` becomes a parameter -// } -// -// -// This enables a HTTP JSON to RPC mapping as below: -// -// HTTP | RPC -// -----|----- -// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))` -// -// Note that fields which are mapped to HTTP parameters must have a -// primitive type or a repeated primitive type. Message types are not -// allowed. In the case of a repeated type, the parameter can be -// repeated in the URL, as in `...?param=A¶m=B`. -// -// For HTTP method kinds which allow a request body, the `body` field -// specifies the mapping. Consider a REST update method on the -// message resource collection: -// -// -// service Messaging { -// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { -// option (google.api.http) = { -// put: "/v1/messages/{message_id}" -// body: "message" -// }; -// } -// } -// message UpdateMessageRequest { -// string message_id = 1; // mapped to the URL -// Message message = 2; // mapped to the body -// } -// -// -// The following HTTP JSON to RPC mapping is enabled, where the -// representation of the JSON in the request body is determined by -// protos JSON encoding: -// -// HTTP | RPC -// -----|----- -// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })` -// -// The special name `*` can be used in the body mapping to define that -// every field not bound by the path template should be mapped to the -// request body. This enables the following alternative definition of -// the update method: -// -// service Messaging { -// rpc UpdateMessage(Message) returns (Message) { -// option (google.api.http) = { -// put: "/v1/messages/{message_id}" -// body: "*" -// }; -// } -// } -// message Message { -// string message_id = 1; -// string text = 2; -// } -// -// -// The following HTTP JSON to RPC mapping is enabled: -// -// HTTP | RPC -// -----|----- -// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")` -// -// Note that when using `*` in the body mapping, it is not possible to -// have HTTP parameters, as all fields not bound by the path end in -// the body. This makes this option more rarely used in practice of -// defining REST APIs. The common usage of `*` is in custom methods -// which don't use the URL at all for transferring data. -// -// It is possible to define multiple HTTP methods for one RPC by using -// the `additional_bindings` option. Example: -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http) = { -// get: "/v1/messages/{message_id}" -// additional_bindings { -// get: "/v1/users/{user_id}/messages/{message_id}" -// } -// }; -// } -// } -// message GetMessageRequest { -// string message_id = 1; -// string user_id = 2; -// } -// -// -// This enables the following two alternative HTTP JSON to RPC -// mappings: -// -// HTTP | RPC -// -----|----- -// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` -// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")` -// -// # Rules for HTTP mapping -// -// The rules for mapping HTTP path, query parameters, and body fields -// to the request message are as follows: -// -// 1. The `body` field specifies either `*` or a field path, or is -// omitted. If omitted, it indicates there is no HTTP request body. -// 2. Leaf fields (recursive expansion of nested messages in the -// request) can be classified into three types: -// (a) Matched in the URL template. -// (b) Covered by body (if body is `*`, everything except (a) fields; -// else everything under the body field) -// (c) All other fields. -// 3. URL query parameters found in the HTTP request are mapped to (c) fields. -// 4. Any body sent with an HTTP request can contain only (b) fields. -// -// The syntax of the path template is as follows: -// -// Template = "/" Segments [ Verb ] ; -// Segments = Segment { "/" Segment } ; -// Segment = "*" | "**" | LITERAL | Variable ; -// Variable = "{" FieldPath [ "=" Segments ] "}" ; -// FieldPath = IDENT { "." IDENT } ; -// Verb = ":" LITERAL ; -// -// The syntax `*` matches a single path segment. The syntax `**` matches zero -// or more path segments, which must be the last part of the path except the -// `Verb`. The syntax `LITERAL` matches literal text in the path. -// -// The syntax `Variable` matches part of the URL path as specified by its -// template. A variable template must not contain other variables. If a variable -// matches a single path segment, its template may be omitted, e.g. `{var}` -// is equivalent to `{var=*}`. -// -// If a variable contains exactly one path segment, such as `"{var}"` or -// `"{var=*}"`, when such a variable is expanded into a URL path, all characters -// except `[-_.~0-9a-zA-Z]` are percent-encoded. Such variables show up in the -// Discovery Document as `{var}`. -// -// If a variable contains one or more path segments, such as `"{var=foo/*}"` -// or `"{var=**}"`, when such a variable is expanded into a URL path, all -// characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. Such variables -// show up in the Discovery Document as `{+var}`. -// -// NOTE: While the single segment variable matches the semantics of -// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 -// Simple String Expansion, the multi segment variable **does not** match -// RFC 6570 Reserved Expansion. The reason is that the Reserved Expansion -// does not expand special characters like `?` and `#`, which would lead -// to invalid URLs. -// -// NOTE: the field paths in variables and in the `body` must not refer to -// repeated fields or map fields. -message HttpRule { - // Selects methods to which this rule applies. - // - // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - string selector = 1; - - // Determines the URL pattern is matched by this rules. This pattern can be - // used with any of the {get|put|post|delete|patch} methods. A custom method - // can be defined using the 'custom' field. - oneof pattern { - // Used for listing and getting information about resources. - string get = 2; - - // Used for updating a resource. - string put = 3; - - // Used for creating a resource. - string post = 4; - - // Used for deleting a resource. - string delete = 5; - - // Used for updating a resource. - string patch = 6; - - // The custom pattern is used for specifying an HTTP method that is not - // included in the `pattern` field, such as HEAD, or "*" to leave the - // HTTP method unspecified for this rule. The wild-card rule is useful - // for services that provide content to Web (HTML) clients. - CustomHttpPattern custom = 8; - } - - // The name of the request field whose value is mapped to the HTTP body, or - // `*` for mapping all fields not captured by the path pattern to the HTTP - // body. NOTE: the referred field must not be a repeated field and must be - // present at the top-level of request message type. - string body = 7; - - // Optional. The name of the response field whose value is mapped to the HTTP - // body of response. Other response fields are ignored. When - // not set, the response message will be used as HTTP body of response. - string response_body = 12; - - // Additional HTTP bindings for the selector. Nested bindings must - // not contain an `additional_bindings` field themselves (that is, - // the nesting may only be one level deep). - repeated HttpRule additional_bindings = 11; -} - -// A custom pattern is used for defining custom HTTP verb. -message CustomHttpPattern { - // The name of this custom HTTP verb. - string kind = 1; - - // The path matched by this custom verb. - string path = 2; -} From a2112166e0be22e53c9c4d26da2a1f95c9ebbe13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 10 Jan 2025 11:24:47 -0300 Subject: [PATCH 033/104] Add proto submodule and build instructions --- .gitmodules | 3 ++ Cargo.lock | 76 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + crates/signer/Cargo.toml | 3 ++ crates/signer/README.md | 12 +++++++ crates/signer/build.rs | 12 +++++++ crates/signer/proto | 1 + 7 files changed, 108 insertions(+) create mode 100644 .gitmodules create mode 100644 crates/signer/README.md create mode 100644 crates/signer/build.rs create mode 160000 crates/signer/proto diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..fd26c6f2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crates/signer/proto"] + path = crates/signer/proto + url = https://github.com/wealdtech/eth2-signer-api.git diff --git a/Cargo.lock b/Cargo.lock index 9dcc49d7..eeb4cbf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1398,6 +1398,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tonic", + "tonic-build", "tracing", "tree_hash 0.8.0", "tree_hash_derive", @@ -2227,6 +2228,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fnv" version = "1.0.7" @@ -3104,6 +3111,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + [[package]] name = "native-tls" version = "0.2.12" @@ -3406,6 +3419,16 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.7.0", +] + [[package]] name = "pharos" version = "0.5.3" @@ -3479,6 +3502,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.93", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -3577,6 +3610,26 @@ dependencies = [ "prost-derive", ] +[[package]] +name = "prost-build" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" +dependencies = [ + "heck", + "itertools 0.13.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.93", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.13.4" @@ -3590,6 +3643,15 @@ dependencies = [ "syn 2.0.93", ] +[[package]] +name = "prost-types" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +dependencies = [ + "prost", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -4780,6 +4842,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.93", +] + [[package]] name = "tower" version = "0.4.13" diff --git a/Cargo.toml b/Cargo.toml index 8cf807e6..534d72a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ axum-extra = { version = "0.9.3", features = ["typed-header"] } reqwest = { version = "0.12.4", features = ["json", "stream"] } headers = "0.4.0" tonic = { version = "0.12.3", features = ["tls", "channel", "prost"] } +tonic-build = "0.12.3" prost = "0.13.4" # async / threads diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index e5a31bce..a2a9ac7a 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -40,3 +40,6 @@ bimap.workspace = true lazy_static.workspace = true derive_more.workspace = true rand.workspace = true + +[build-dependencies] +tonic-build.workspace = true diff --git a/crates/signer/README.md b/crates/signer/README.md new file mode 100644 index 00000000..f173f0cd --- /dev/null +++ b/crates/signer/README.md @@ -0,0 +1,12 @@ +# Signer Module + +## Compile proto files + +### Requirements + +- `protoc` compiler +- Submodules initialized: `git submodule update --init` + +To compile the `.proto` files and generate the Rust bindings, simply run `cargo build`. This will only run if there are changes to the `.proto` files. If you want to force a rebuild, run `cargo clean` first. + +Note that this process is not required to run Commit-Boost as the generated files are already included in the repository. diff --git a/crates/signer/build.rs b/crates/signer/build.rs new file mode 100644 index 00000000..596353ea --- /dev/null +++ b/crates/signer/build.rs @@ -0,0 +1,12 @@ +fn main() -> Result<(), Box> { + std::env::set_var("OUT_DIR", "src/proto"); + tonic_build::configure().build_server(false).compile_protos( + &[ + "proto/pb/v1/lister.proto", + "proto/pb/v1/accountmanager.proto", + "proto/pb/v1/signer.proto", + ], + &["proto/pb/v1", "proto/third-party"], + )?; + Ok(()) +} diff --git a/crates/signer/proto b/crates/signer/proto new file mode 160000 index 00000000..4aaf36e5 --- /dev/null +++ b/crates/signer/proto @@ -0,0 +1 @@ +Subproject commit 4aaf36e54f4e62d0cf4edc1b794e9a6354cf4f95 From f4bd771e1c93732772f14244796cafa69dfb5e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 10 Jan 2025 11:33:07 -0300 Subject: [PATCH 034/104] Update CI --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5677b703..d003740c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,8 @@ jobs: uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable + - name: Install protoc + run: sudo apt-get install protobuf-compiler - run: cargo clippy --all -- -D warnings tests: name: Test @@ -33,4 +35,6 @@ jobs: uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable + - name: Install protoc + run: sudo apt-get install protobuf-compiler - run: cargo test --all-features From 11150a1024007620aae6f3f8c77f9b20e99b55fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 10 Jan 2025 11:38:35 -0300 Subject: [PATCH 035/104] Update CI --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d003740c..21adb771 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,8 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + with: + submodules: true - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable - name: Install protoc @@ -33,6 +35,8 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + with: + submodules: true - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable - name: Install protoc From 755b2b50312bea28fdef5faac1e7dd59f74c9af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 14 Jan 2025 15:49:18 -0300 Subject: [PATCH 036/104] Save delegation signatures on local store --- crates/common/src/config/signer.rs | 5 ++++- crates/common/src/signer/store.rs | 26 ++++++++++++++++++++++++++ crates/signer/src/manager/dirk.rs | 23 ++++++++++++++++++----- crates/signer/src/service.rs | 17 +++++++++++------ 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 573f0239..d9b1e6b1 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -66,6 +66,8 @@ pub enum SignerType { /// Whether to unlock the accounts in case they are locked #[serde(default)] unlock: bool, + /// How to store proxy keys + store: Option, }, } @@ -118,6 +120,7 @@ impl StartSignerConfig { ca_cert_path, server_domain, unlock, + store, .. } => { let cert_path = load_env_var(DIRK_CERT_ENV).map(PathBuf::from).unwrap_or(cert_path); @@ -132,7 +135,7 @@ impl StartSignerConfig { server_port, jwts, loader: None, - store: None, + store, dirk: Some(DirkConfig { url, wallets, diff --git a/crates/common/src/signer/store.rs b/crates/common/src/signer/store.rs index 3127d254..a5563019 100644 --- a/crates/common/src/signer/store.rs +++ b/crates/common/src/signer/store.rs @@ -153,6 +153,32 @@ impl ProxyStore { Ok(()) } + pub fn store_proxy_bls_delegation( + &self, + module_id: &ModuleId, + delegation: &SignedProxyDelegation, + ) -> eyre::Result<()> { + let base_path = match self { + ProxyStore::File { proxy_dir } => proxy_dir, + ProxyStore::ERC2335 { keys_path, .. } => keys_path, + }; + let file_path = base_path + .join("delegations") + .join(module_id.to_string()) + .join("bls") + .join(delegation.message.proxy.to_string()); + let content = serde_json::to_vec(&delegation)?; + + if let Some(parent) = file_path.parent() { + create_dir_all(parent)?; + } + + let mut file = std::fs::File::create(file_path)?; + file.write_all(content.as_ref())?; + + Ok(()) + } + #[allow(clippy::type_complexity)] pub fn load_proxies( &self, diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index cfa6fe42..2063b565 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -6,12 +6,13 @@ use cb_common::{ config::DirkConfig, constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, - signer::{BlsPublicKey, BlsSignature}, + signer::{BlsPublicKey, BlsSignature, ProxyStore}, types::{Chain, ModuleId}, }; use rand::Rng; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::info; +use tree_hash::TreeHash; use crate::proto::v1::{ account_manager_client::AccountManagerClient, lister_client::ListerClient, @@ -26,6 +27,7 @@ pub struct DirkManager { wallets: Vec, unlock: bool, secrets_path: PathBuf, + proxy_store: Option, } impl DirkManager { @@ -54,9 +56,14 @@ impl DirkManager { wallets: config.wallets, unlock: config.unlock, secrets_path: config.secrets_path, + proxy_store: None, }) } + pub fn with_proxy_store(self, proxy_store: ProxyStore) -> eyre::Result { + Ok(Self { proxy_store: Some(proxy_store), ..self }) + } + async fn get_all_accounts(&self) -> eyre::Result> { let mut client = ListerClient::new(self.channel.clone()); let pubkeys_request = @@ -248,10 +255,16 @@ impl DirkManager { self.unlock_account(account_name, new_password).await?; - Ok(SignedProxyDelegation { - message: ProxyDelegation { delegator: consensus_pubkey, proxy: proxy_key }, - signature: BlsSignature::ZERO, - }) + let message = ProxyDelegation { delegator: consensus_pubkey, proxy: proxy_key }; + let signature = + self.request_signature(consensus_pubkey, message.tree_hash_root().0).await?; + let delegation = SignedProxyDelegation { message, signature }; + + if let Some(store) = &self.proxy_store { + store.store_proxy_bls_delegation(&module_id, &delegation)?; + } + + Ok(delegation) } pub async fn request_signature( diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index ad5b5fff..014b7199 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -59,12 +59,17 @@ impl SigningService { let module_ids: Vec = config.jwts.left_values().cloned().map(Into::into).collect(); let state = match config.dirk { - Some(dirk) => SigningState { - manager: SigningManager::Dirk( - DirkManager::new_from_config(config.chain, dirk).await?, - ), - jwts: config.jwts.into(), - }, + Some(dirk) => { + let mut dirk_manager = DirkManager::new_from_config(config.chain, dirk).await?; + if let Some(store) = config.store { + dirk_manager = dirk_manager.with_proxy_store(store.init_from_env()?)?; + } + + SigningState { + manager: SigningManager::Dirk(dirk_manager), + jwts: config.jwts.into(), + } + } None => { let proxy_store = if let Some(store) = config.store { Some(store.init_from_env()?) From c0445b35d21a0c7c2f05df722b8bab969cb9acd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 14 Jan 2025 16:19:33 -0300 Subject: [PATCH 037/104] Update docs --- docs/docs/get_started/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 719cbe53..5c7ae8d5 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -259,7 +259,7 @@ unlock = false - `cert_path` and `key_path` are the paths to the client certificate and key used to authenticate with Dirk. - `wallets` is a list of wallets that the Signer module will use to sign transactions. Each wallet should have a `/consensus` account which will be used as the consensus key. Generated proxy keys will be stored in `//`. -- `secrets_path` is the path to the folder containing the passwords of the accounts. Generated proxy accounts secrets will be stored in `///`. +- `secrets_path` is the path to the folder containing the passwords of the accounts. Passwords must be in plain text in files with structure `//`. Generated proxy accounts secrets will be stored in `///`. - `unlock` is an optional parameter that can be set to `true` if you want to try to unlock the wallets on sign failure. Default is `false`. ## Custom module From 2ebe46cef6cbcc43e680b34ceedf9e7a2b224f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 14 Jan 2025 19:37:37 -0300 Subject: [PATCH 038/104] Add protoc to Dockerfiles --- examples/builder_log/Dockerfile | 2 ++ examples/da_commit/Dockerfile | 2 ++ examples/status_api/Dockerfile | 2 ++ provisioning/pbs.Dockerfile | 2 ++ provisioning/signer.Dockerfile | 2 ++ 5 files changed, 10 insertions(+) diff --git a/examples/builder_log/Dockerfile b/examples/builder_log/Dockerfile index 2c1353a2..c0fd69b4 100644 --- a/examples/builder_log/Dockerfile +++ b/examples/builder_log/Dockerfile @@ -10,6 +10,8 @@ COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json +RUN apt-get update && apt-get install -y protobuf-compiler + COPY . . RUN cargo build --release --bin builder_log diff --git a/examples/da_commit/Dockerfile b/examples/da_commit/Dockerfile index 25c88dc8..5c513848 100644 --- a/examples/da_commit/Dockerfile +++ b/examples/da_commit/Dockerfile @@ -10,6 +10,8 @@ COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json +RUN apt-get update && apt-get install -y protobuf-compiler + COPY . . RUN cargo build --release --bin da_commit diff --git a/examples/status_api/Dockerfile b/examples/status_api/Dockerfile index f6f5568f..dd20f000 100644 --- a/examples/status_api/Dockerfile +++ b/examples/status_api/Dockerfile @@ -10,6 +10,8 @@ COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json +RUN apt-get update && apt-get install -y protobuf-compiler + COPY . . RUN cargo build --release --bin status_api diff --git a/provisioning/pbs.Dockerfile b/provisioning/pbs.Dockerfile index 95568b9d..200c95d2 100644 --- a/provisioning/pbs.Dockerfile +++ b/provisioning/pbs.Dockerfile @@ -10,6 +10,8 @@ COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json +RUN apt-get update && apt-get install -y protobuf-compiler + COPY . . RUN cargo build --release --bin commit-boost-pbs diff --git a/provisioning/signer.Dockerfile b/provisioning/signer.Dockerfile index 51c192af..85c2be43 100644 --- a/provisioning/signer.Dockerfile +++ b/provisioning/signer.Dockerfile @@ -10,6 +10,8 @@ COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json +RUN apt-get update && apt-get install -y protobuf-compiler + COPY . . RUN cargo build --release --bin commit-boost-signer From f0e37711d8b3c3d568d910485a76969423e7b685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 14 Jan 2025 19:38:03 -0300 Subject: [PATCH 039/104] Add proxy store config to docker_init --- crates/cli/src/docker_init.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index b01cab68..00f0eba8 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -450,7 +450,7 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu services.insert("cb_signer".to_owned(), Some(signer_service)); } } - SignerType::Dirk { cert_path, key_path, secrets_path, ca_cert_path, .. } => { + SignerType::Dirk { cert_path, key_path, secrets_path, ca_cert_path, store, .. } => { if needs_signer_module { if metrics_enabled { targets.push(PrometheusTargetConfig { @@ -507,6 +507,22 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu signer_envs.insert(key, val); } + match store { + Some(ProxyStore::File { proxy_dir }) => { + volumes.push(Volumes::Simple(format!( + "{}:{}", + proxy_dir.display(), + PROXY_DIR_DEFAULT + ))); + let (key, val) = get_env_val(PROXY_DIR_ENV, PROXY_DIR_DEFAULT); + signer_envs.insert(key, val); + } + Some(ProxyStore::ERC2335 { .. }) => { + panic!("ERC2335 store not supported with Dirk signer"); + } + None => {} + } + // networks let mut signer_networks = vec![SIGNER_NETWORK.to_owned()]; if metrics_enabled { From 02e74b504030d11cdad5888f05180128f9487139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 14 Jan 2025 19:38:38 -0300 Subject: [PATCH 040/104] Refactor manager to use accounts instead of wallets --- crates/common/src/config/signer.rs | 10 +- crates/signer/src/manager/dirk.rs | 216 +++++++++++++++++-------- docs/docs/get_started/configuration.md | 4 +- 3 files changed, 158 insertions(+), 72 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index d9b1e6b1..20f9b19c 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -55,8 +55,8 @@ pub enum SignerType { cert_path: PathBuf, /// Path to the client key key_path: PathBuf, - /// Wallets to use. Each wallet should have a `wallet/consensus` account - wallets: Vec, + /// Accounts used as consensus keys + accounts: Vec, /// Path to where the account passwords are stored secrets_path: PathBuf, /// Path to the CA certificate @@ -74,7 +74,7 @@ pub enum SignerType { #[derive(Clone, Debug)] pub struct DirkConfig { pub url: Url, - pub wallets: Vec, + pub accounts: Vec, pub client_cert: Identity, pub secrets_path: PathBuf, pub cert_auth: Option, @@ -115,7 +115,7 @@ impl StartSignerConfig { url, cert_path, key_path, - wallets, + accounts, secrets_path, ca_cert_path, server_domain, @@ -138,7 +138,7 @@ impl StartSignerConfig { store, dirk: Some(DirkConfig { url, - wallets, + accounts, client_cert: Identity::from_pem( std::fs::read_to_string(cert_path)?, std::fs::read_to_string(key_path)?, diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 2063b565..a6e0fc7f 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -20,11 +20,24 @@ use crate::proto::v1::{ GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, UnlockAccountRequest, }; +#[derive(Clone, Debug)] +struct Account { + wallet: String, + name: String, + public_key: Option, +} + +impl Account { + pub fn complete_name(&self) -> String { + format!("{}/{}", self.wallet, self.name) + } +} + #[derive(Clone, Debug)] pub struct DirkManager { chain: Chain, channel: Channel, - wallets: Vec, + accounts: Vec, unlock: bool, secrets_path: PathBuf, proxy_store: Option, @@ -50,10 +63,51 @@ impl DirkManager { .await .map_err(|e| eyre::eyre!("Couldn't connect to Dirk: {e}"))?; + let dirk_accounts = get_accounts_in_wallets( + channel.clone(), + config + .accounts + .iter() + .filter_map(|account| Some(account.split_once("/")?.0.to_string())) + .collect(), + ) + .await?; + + let mut accounts = Vec::with_capacity(config.accounts.len()); + for account in config.accounts { + let (wallet, name) = account.split_once("/").ok_or(eyre::eyre!( + "Invalid account name: {account}. It must be in format wallet/account" + ))?; + let public_key = dirk_accounts.iter().find_map(|a| { + if a.name == account { + BlsPublicKey::try_from(a.public_key.as_slice()).ok() + } else { + None + } + }); + + accounts.push(Account { + wallet: wallet.to_string(), + name: name.to_string(), + public_key, + }); + } + let wallets = + accounts.iter().map(|account| account.wallet.clone()).collect::>(); + let dirk_accounts = get_accounts_in_wallets(channel.clone(), wallets).await?; + for account in accounts.iter_mut() { + if let Some(dirk_account) = + dirk_accounts.iter().find(|a| a.name == account.complete_name()) + { + account.public_key = + Some(BlsPublicKey::try_from(dirk_account.public_key.as_slice())?); + } + } + Ok(Self { chain, channel, - wallets: config.wallets, + accounts, unlock: config.unlock, secrets_path: config.secrets_path, proxy_store: None, @@ -64,77 +118,81 @@ impl DirkManager { Ok(Self { proxy_store: Some(proxy_store), ..self }) } + /// Get all available accounts in the `self.accounts` wallets async fn get_all_accounts(&self) -> eyre::Result> { - let mut client = ListerClient::new(self.channel.clone()); - let pubkeys_request = - tonic::Request::new(ListAccountsRequest { paths: self.wallets.clone() }); - let pubkeys_response = client.list_accounts(pubkeys_request).await?; - - if pubkeys_response.get_ref().state() != ResponseState::Succeeded { - eyre::bail!("Get pubkeys request failed".to_string()); - } - - Ok(pubkeys_response.into_inner().accounts) + get_accounts_in_wallets( + self.channel.clone(), + self.accounts.iter().map(|account| account.wallet.clone()).collect::>(), + ) + .await } + /// Get the complete account name (`wallet/account`) for a public key. + /// Returns `Ok(None)` if the account was not found. + /// Returns `Err` if there was a communication error with Dirk. async fn get_pubkey_account(&self, pubkey: BlsPublicKey) -> eyre::Result> { - let accounts = self.get_all_accounts().await?; + match self + .accounts + .iter() + .find(|account| account.public_key.is_some_and(|account_pk| account_pk == pubkey)) + { + Some(account) => Ok(Some(account.complete_name())), + None => { + let accounts = self.get_all_accounts().await?; + + for account in accounts { + if account.public_key == pubkey.to_vec() { + return Ok(Some(account.name)); + } + } - for account in accounts { - if account.public_key == pubkey.to_vec() { - return Ok(Some(account.name)); + Ok(None) } } - - Ok(None) - } - - async fn get_pubkey_wallet(&self, pubkey: BlsPublicKey) -> eyre::Result> { - let account = self.get_pubkey_account(pubkey).await?; - - if let Some(account) = account { - Ok(Some( - account - .split_once("/") - .ok_or(eyre::eyre!( - "Invalid account name: {account}. It must be in format wallet/account" - ))? - .0 - .to_string(), - )) - } else { - Ok(None) - } } + /// Returns the public keys of the config-registered accounts pub async fn consensus_pubkeys(&self) -> eyre::Result> { - let accounts = self.get_all_accounts().await?; + let registered_pubkeys = self + .accounts + .iter() + .filter_map(|account| account.public_key) + .collect::>(); - let expected_accounts: Vec = - self.wallets.iter().map(|wallet| format!("{wallet}/consensus")).collect(); + if registered_pubkeys.len() == self.accounts.len() { + Ok(registered_pubkeys) + } else { + let accounts = self.get_all_accounts().await?; - Ok(accounts - .iter() - .filter_map(|account| { - if expected_accounts.contains(&account.name) { - BlsPublicKey::try_from(account.public_key.as_slice()).ok() - } else { - None - } - }) - .collect()) + let expected_accounts: Vec = + self.accounts.iter().map(|account| account.complete_name()).collect(); + + Ok(accounts + .iter() + .filter_map(|account| { + if expected_accounts.contains(&account.name) { + BlsPublicKey::try_from(account.public_key.as_slice()).ok() + } else { + None + } + }) + .collect()) + } } + /// Returns the public keys of all the proxy accounts found in Dirk. + /// An account is considered a proxy if its name has the format + /// `consensus_account/module_id/uuid`, where `consensus_account` is the + /// name of a config-registered account. pub async fn proxies(&self) -> eyre::Result> { let accounts = self.get_all_accounts().await?; Ok(accounts .iter() .filter_map(|account| { - let wallet = account.name.split_once("/")?.0; - if self.wallets.contains(&wallet.to_string()) && - account.name != format!("{wallet}/consensus") - { + if self.accounts.iter().any(|consensus_account| { + account.name.starts_with(&format!("{}/", consensus_account.complete_name())) + }) { BlsPublicKey::try_from(account.public_key.as_slice()).ok() } else { None @@ -143,6 +201,11 @@ impl DirkManager { .collect()) } + /// Returns a mapping of the proxy accounts' pubkeys by consensus account, + /// for a given module. + /// An account is considered a proxy if its name has the format + /// `consensus_account/module_id/uuid`, where `consensus_account` is the + /// name of a config-registered account. pub async fn get_consensus_proxy_maps( &self, module_id: &ModuleId, @@ -151,21 +214,18 @@ impl DirkManager { let mut proxy_maps = Vec::new(); - for wallet in self.wallets.iter() { - let Some(consensus_key) = accounts.iter().find_map(|account| { - if account.name == format!("{wallet}/consensus") { - BlsPublicKey::try_from(account.public_key.as_slice()).ok() - } else { - None - } - }) else { + for consensus_account in self.accounts.iter() { + let Some(consensus_key) = consensus_account.public_key else { continue; }; let proxy_keys = accounts .iter() .filter_map(|account| { - if account.name.starts_with(&format!("{wallet}/{module_id}")) { + if account + .name + .starts_with(&format!("{}/{module_id}/", consensus_account.complete_name())) + { BlsPublicKey::try_from(account.public_key.as_slice()).ok() } else { None @@ -227,12 +287,22 @@ impl DirkManager { ) -> eyre::Result> { let uuid = uuid::Uuid::new_v4(); - let wallet = self - .get_pubkey_wallet(consensus_pubkey) + let consensus_account = self + .get_pubkey_account(consensus_pubkey) .await? .ok_or(eyre::eyre!("Consensus public key not found"))?; - let account_name = format!("{wallet}/{module_id}/{uuid}"); + if !self + .accounts + .iter() + .map(|account| account.complete_name()) + .collect::>() + .contains(&consensus_account) + { + eyre::bail!("Consensus public key is not from a registered account"); + } + + let account_name = format!("{consensus_account}/{module_id}/{uuid}"); let new_password = Self::random_password(); let mut client = AccountManagerClient::new(self.channel.clone()); @@ -314,3 +384,19 @@ impl DirkManager { )?)) } } + +/// Get the accounts for the wallets passed as argument +async fn get_accounts_in_wallets( + channel: Channel, + wallets: Vec, +) -> eyre::Result> { + let mut client = ListerClient::new(channel); + let pubkeys_request = tonic::Request::new(ListAccountsRequest { paths: wallets }); + let pubkeys_response = client.list_accounts(pubkeys_request).await?; + + if pubkeys_response.get_ref().state() != ResponseState::Succeeded { + eyre::bail!("Get pubkeys request failed".to_string()); + } + + Ok(pubkeys_response.into_inner().accounts) +} diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 5c7ae8d5..36b61cc2 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -248,7 +248,7 @@ Dirk is a distributed key management system that can be used to sign transaction url = "https://dirk.gateway.url" cert_path = "/path/to/client.crt" key_path = "/path/to/client.key" -wallets = ["wallet1", "wallet2"] +accounts = ["wallet1/accountX", "wallet2/accountY"] secrets_path = "/path/to/secrets" # Optional parameters @@ -258,7 +258,7 @@ unlock = false ``` - `cert_path` and `key_path` are the paths to the client certificate and key used to authenticate with Dirk. -- `wallets` is a list of wallets that the Signer module will use to sign transactions. Each wallet should have a `/consensus` account which will be used as the consensus key. Generated proxy keys will be stored in `//`. +- `accounts` is a list of accounts that the Signer module will consider as the consensus keys. Each account has the format `/`. Accounts can be from different wallets. Generated proxy keys will have format `///`. - `secrets_path` is the path to the folder containing the passwords of the accounts. Passwords must be in plain text in files with structure `//`. Generated proxy accounts secrets will be stored in `///`. - `unlock` is an optional parameter that can be set to `true` if you want to try to unlock the wallets on sign failure. Default is `false`. From c23413eeadd87a39f4c01a2eb3ffbf9e348d5dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 15 Jan 2025 15:05:52 -0300 Subject: [PATCH 041/104] Improve logs --- crates/common/src/commit/request.rs | 27 +++++- crates/signer/src/error.rs | 29 ++++-- crates/signer/src/manager/dirk.rs | 138 +++++++++++++++++++--------- crates/signer/src/manager/local.rs | 7 +- crates/signer/src/manager/mod.rs | 4 +- crates/signer/src/service.rs | 59 ++++++------ 6 files changed, 179 insertions(+), 85 deletions(-) diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index b45861f0..0d7157cc 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -3,7 +3,7 @@ use std::{ str::FromStr, }; -use alloy::rpc::types::beacon::BlsSignature; +use alloy::{hex, rpc::types::beacon::BlsSignature}; use derive_more::derive::From; use serde::{Deserialize, Serialize}; use tree_hash::TreeHash; @@ -78,6 +78,31 @@ pub enum SignRequest { ProxyEcdsa(SignProxyRequest), } +impl Display for SignRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SignRequest::Consensus(req) => write!( + f, + "Consensus(pubkey: {}, object_root: {})", + req.pubkey, + hex::encode(&req.object_root) + ), + SignRequest::ProxyBls(req) => write!( + f, + "BLS(pubkey: {}, object_root: {})", + req.pubkey, + hex::encode(&req.object_root) + ), + SignRequest::ProxyEcdsa(req) => write!( + f, + "ECDSA(pubkey: {}, object_root: {})", + req.pubkey, + hex::encode(&req.object_root) + ), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignConsensusRequest { pub pubkey: BlsPublicKey, diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index 39f59cd1..a60dd9fa 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -16,23 +16,32 @@ pub enum SignerModuleError { #[error("unknown proxy signer: 0x{}", hex::encode(.0))] UnknownProxySigner(Vec), + #[error("Dirk communication error: {0}")] + DirkCommunicationError(String), + #[error("Dirk signer does not support this operation")] DirkNotSupported, - #[error("internal error {0}")] + #[error("internal error: {0}")] Internal(String), } impl IntoResponse for SignerModuleError { fn into_response(self) -> Response { - let status = match self { - SignerModuleError::Unauthorized => StatusCode::UNAUTHORIZED, - SignerModuleError::UnknownConsensusSigner(_) => StatusCode::NOT_FOUND, - SignerModuleError::UnknownProxySigner(_) => StatusCode::NOT_FOUND, - SignerModuleError::DirkNotSupported => StatusCode::BAD_REQUEST, - SignerModuleError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - }; - - (status, self.to_string()).into_response() + match self { + SignerModuleError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()), + SignerModuleError::UnknownConsensusSigner(_) => { + (StatusCode::NOT_FOUND, self.to_string()) + } + SignerModuleError::UnknownProxySigner(_) => (StatusCode::NOT_FOUND, self.to_string()), + SignerModuleError::DirkCommunicationError(_) => { + (StatusCode::BAD_GATEWAY, "Dirk communication error".to_string()) + } + SignerModuleError::DirkNotSupported => (StatusCode::BAD_REQUEST, self.to_string()), + SignerModuleError::Internal(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, "internal error".to_string()) + } + } + .into_response() } } diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index a6e0fc7f..7b669476 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -14,10 +14,13 @@ use tonic::transport::{Channel, ClientTlsConfig}; use tracing::info; use tree_hash::TreeHash; -use crate::proto::v1::{ - account_manager_client::AccountManagerClient, lister_client::ListerClient, - sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, - GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, UnlockAccountRequest, +use crate::{ + error::SignerModuleError::{self, DirkCommunicationError}, + proto::v1::{ + account_manager_client::AccountManagerClient, lister_client::ListerClient, + sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, + GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, UnlockAccountRequest, + }, }; #[derive(Clone, Debug)] @@ -119,7 +122,7 @@ impl DirkManager { } /// Get all available accounts in the `self.accounts` wallets - async fn get_all_accounts(&self) -> eyre::Result> { + async fn get_all_accounts(&self) -> Result, SignerModuleError> { get_accounts_in_wallets( self.channel.clone(), self.accounts.iter().map(|account| account.wallet.clone()).collect::>(), @@ -130,7 +133,10 @@ impl DirkManager { /// Get the complete account name (`wallet/account`) for a public key. /// Returns `Ok(None)` if the account was not found. /// Returns `Err` if there was a communication error with Dirk. - async fn get_pubkey_account(&self, pubkey: BlsPublicKey) -> eyre::Result> { + async fn get_pubkey_account( + &self, + pubkey: BlsPublicKey, + ) -> Result, SignerModuleError> { match self .accounts .iter() @@ -209,7 +215,7 @@ impl DirkManager { pub async fn get_consensus_proxy_maps( &self, module_id: &ModuleId, - ) -> eyre::Result> { + ) -> Result, SignerModuleError> { let accounts = self.get_all_accounts().await?; let mut proxy_maps = Vec::new(); @@ -249,32 +255,57 @@ impl DirkManager { } /// Read the password for an account from a file - fn read_password(&self, account: String) -> eyre::Result { - fs::read_to_string(self.secrets_path.join(account)) - .map_err(|e| eyre::eyre!("Couldn't read password: {e}")) + fn read_password(&self, account: String) -> Result { + fs::read_to_string(self.secrets_path.join(account.clone())).map_err(|err| { + SignerModuleError::Internal(format!( + "error reading password for account '{account}': {err}" + )) + }) } /// Store the password for an account in a file - fn store_password(&self, account: String, password: String) -> eyre::Result<()> { - fs::create_dir_all( - self.secrets_path - .join(account.rsplit_once("/").ok_or(eyre::eyre!("Invalid account name"))?.0), - ) - .map_err(|e| eyre::eyre!("Couldn't write password: {e}"))?; - fs::write(self.secrets_path.join(account), password) - .map_err(|e| eyre::eyre!("Couldn't write password: {e}")) + fn store_password(&self, account: String, password: String) -> Result<(), SignerModuleError> { + let account_dir = self + .secrets_path + .join( + account + .rsplit_once("/") + .ok_or(SignerModuleError::Internal(format!( + "account name '{account}' is invalid" + )))? + .0, + ) + .to_string_lossy() + .to_string(); + + fs::create_dir_all(account_dir.clone()).map_err(|err| { + SignerModuleError::Internal(format!("error creating dir '{account_dir}': {err}")) + })?; + fs::write(self.secrets_path.join(account.clone()), password).map_err(|err| { + SignerModuleError::Internal(format!( + "error writing password for account '{account}': {err}" + )) + }) } - async fn unlock_account(&self, account: String, password: String) -> eyre::Result<()> { + async fn unlock_account( + &self, + account: String, + password: String, + ) -> Result<(), SignerModuleError> { let mut client = AccountManagerClient::new(self.channel.clone()); let unlock_request = tonic::Request::new(UnlockAccountRequest { - account, + account: account.clone(), passphrase: password.as_bytes().to_vec(), }); - let unlock_response = client.unlock(unlock_request).await?; + let unlock_response = client.unlock(unlock_request).await.map_err(|err| { + DirkCommunicationError(format!("error unlocking account '{account}': {err}")) + })?; if unlock_response.get_ref().state() != ResponseState::Succeeded { - eyre::bail!("Unlock request failed"); + return Err(DirkCommunicationError(format!( + "unlock request for '{account}' returned error" + ))); } Ok(()) @@ -284,13 +315,13 @@ impl DirkManager { &self, module_id: ModuleId, consensus_pubkey: BlsPublicKey, - ) -> eyre::Result> { + ) -> Result, SignerModuleError> { let uuid = uuid::Uuid::new_v4(); let consensus_account = self .get_pubkey_account(consensus_pubkey) .await? - .ok_or(eyre::eyre!("Consensus public key not found"))?; + .ok_or(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; if !self .accounts @@ -299,7 +330,7 @@ impl DirkManager { .collect::>() .contains(&consensus_account) { - eyre::bail!("Consensus public key is not from a registered account"); + return Err(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; } let account_name = format!("{consensus_account}/{module_id}/{uuid}"); @@ -313,15 +344,21 @@ impl DirkManager { signing_threshold: 1, }); - let generate_response = client.generate(generate_request).await?; + let generate_response = client + .generate(generate_request) + .await + .map_err(|err| DirkCommunicationError(format!("error on generate request: {err}")))?; + if generate_response.get_ref().state() != ResponseState::Succeeded { - eyre::bail!("Generate request failed"); + return Err(DirkCommunicationError("generate request returned error".to_string())); } self.store_password(account_name.clone(), new_password.clone())?; let proxy_key = - BlsPublicKey::try_from(generate_response.into_inner().public_key.as_slice())?; + BlsPublicKey::try_from(generate_response.into_inner().public_key.as_slice()).map_err( + |_| DirkCommunicationError("return value is not a valid public key".to_string()), + )?; self.unlock_account(account_name, new_password).await?; @@ -331,7 +368,9 @@ impl DirkManager { let delegation = SignedProxyDelegation { message, signature }; if let Some(store) = &self.proxy_store { - store.store_proxy_bls_delegation(&module_id, &delegation)?; + store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { + SignerModuleError::Internal(format!("error storing delegation signature: {err}")) + })?; } Ok(delegation) @@ -341,7 +380,7 @@ impl DirkManager { &self, pubkey: BlsPublicKey, object_root: [u8; 32], - ) -> eyre::Result { + ) -> Result { let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); let mut signer_client = SignerClient::new(self.channel.clone()); @@ -351,37 +390,47 @@ impl DirkManager { data: object_root.to_vec(), }); - let sign_response = signer_client.sign(sign_request).await?; + let sign_response = signer_client + .sign(sign_request) + .await + .map_err(|err| DirkCommunicationError(format!("error on sign request: {err}")))?; // Retry if unlock config is set let sign_response = match sign_response.get_ref().state() { ResponseState::Denied if self.unlock => { - info!("Account {pubkey:#} may be locked, unlocking and retrying..."); + info!("Failed to sign message, account {pubkey:#} may be locked. Unlocking and retrying."); let account_name = self .get_pubkey_account(pubkey) .await? - .ok_or(eyre::eyre!("Public key not found"))?; - self.unlock_account(account_name.clone(), self.read_password(account_name)?) - .await?; + .ok_or(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; + self.unlock_account( + account_name.clone(), + self.read_password(account_name.clone())?, + ) + .await?; let sign_request = tonic::Request::new(SignRequest { id: Some(SignerId::PublicKey(pubkey.to_vec())), domain: domain.to_vec(), data: object_root.to_vec(), }); - signer_client.sign(sign_request).await? + signer_client.sign(sign_request).await.map_err(|err| { + DirkCommunicationError(format!("error on sign request: {err}")) + })? } _ => sign_response, }; if sign_response.get_ref().state() != ResponseState::Succeeded { - eyre::bail!("Sign request failed"); + return Err(DirkCommunicationError("sign request returned error".to_string())); } - Ok(BlsSignature::from(FixedBytes::try_from( - sign_response.into_inner().signature.as_slice(), - )?)) + Ok(BlsSignature::from( + FixedBytes::try_from(sign_response.into_inner().signature.as_slice()).map_err( + |_| DirkCommunicationError("return value is not a valid signature".to_string()), + )?, + )) } } @@ -389,13 +438,16 @@ impl DirkManager { async fn get_accounts_in_wallets( channel: Channel, wallets: Vec, -) -> eyre::Result> { +) -> Result, SignerModuleError> { let mut client = ListerClient::new(channel); let pubkeys_request = tonic::Request::new(ListAccountsRequest { paths: wallets }); - let pubkeys_response = client.list_accounts(pubkeys_request).await?; + let pubkeys_response = client + .list_accounts(pubkeys_request) + .await + .map_err(|err| DirkCommunicationError(format!("error listing accounts: {err}")))?; if pubkeys_response.get_ref().state() != ResponseState::Succeeded { - eyre::bail!("Get pubkeys request failed".to_string()); + return Err(DirkCommunicationError("list accounts request returned error".to_string())); } Ok(pubkeys_response.into_inner().accounts) diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index 353221c9..0f9ded3b 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -12,7 +12,6 @@ use cb_common::{ }, types::{Chain, ModuleId}, }; -use eyre::OptionExt; use tree_hash::TreeHash; use crate::error::SignerModuleError; @@ -227,7 +226,7 @@ impl LocalSigningManager { pub fn get_consensus_proxy_maps( &self, module_id: &ModuleId, - ) -> eyre::Result> { + ) -> Result, SignerModuleError> { let consensus = self.consensus_pubkeys(); let proxy_bls = self.proxy_pubkeys_bls.get(module_id).cloned().unwrap_or_default(); let proxy_ecdsa = self.proxy_pubkeys_ecdsa.get(module_id).cloned().unwrap_or_default(); @@ -239,7 +238,7 @@ impl LocalSigningManager { let entry = keys .iter_mut() .find(|x| x.consensus == delegator) - .ok_or_eyre("missing consensus")?; + .ok_or(SignerModuleError::Internal("missing consensus".to_string()))?; entry.proxy_bls.push(bls); } @@ -249,7 +248,7 @@ impl LocalSigningManager { let entry = keys .iter_mut() .find(|x| x.consensus == delegator) - .ok_or_eyre("missing consensus")?; + .ok_or(SignerModuleError::Internal("missing consensus".to_string()))?; entry.proxy_ecdsa.push(ecdsa); } diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index 96946834..959edfc1 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -5,6 +5,8 @@ use dirk::DirkManager; use local::LocalSigningManager; use tokio::sync::RwLock; +use crate::error::SignerModuleError; + pub mod dirk; pub mod local; @@ -40,7 +42,7 @@ impl SigningManager { pub async fn get_consensus_proxy_maps( &self, module_id: &ModuleId, - ) -> eyre::Result> { + ) -> Result, SignerModuleError> { match self { SigningManager::Local(local_manager) => { local_manager.read().await.get_consensus_proxy_maps(module_id) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 954f20a4..1d03b108 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -106,15 +106,13 @@ impl SigningService { .route(GENERATE_PROXY_KEY_PATH, post(handle_generate_proxy)) .with_state(state.clone()) .route_layer(middleware::from_fn_with_state(state.clone(), jwt_auth)) - .route_layer(middleware::from_fn(log_request)); - let status_router = axum::Router::new().route(STATUS_PATH, get(handle_status)); + .route_layer(middleware::from_fn(log_request)) + .route(STATUS_PATH, get(handle_status)); let address = SocketAddr::from(([0, 0, 0, 0], config.server_port)); let listener = TcpListener::bind(address).await?; - axum::serve(listener, axum::Router::new().merge(app).merge(status_router)) - .await - .wrap_err("signer server exited") + axum::serve(listener, app).await.wrap_err("signer server exited") } fn init_metrics(network: Chain) -> Result<()> { @@ -182,17 +180,16 @@ async fn handle_request_signature( ) -> Result { let req_id = Uuid::new_v4(); - debug!(event = "request_signature", ?module_id, ?req_id, "New request"); + debug!(event = "request_signature", ?module_id, %request, ?req_id, "New request"); - match state.manager { + let res = match state.manager { SigningManager::Local(local_manager) => match request { SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => local_manager .read() .await .sign_consensus(&pubkey, &object_root) .await - .map(|sig| Json(sig).into_response()) - .map_err(|err| SignerModuleError::Internal(err.to_string())), + .map(|sig| Json(sig).into_response()), SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { local_manager .read() @@ -200,7 +197,6 @@ async fn handle_request_signature( .sign_proxy_bls(&bls_key, &object_root) .await .map(|sig| Json(sig).into_response()) - .map_err(|err| SignerModuleError::Internal(err.to_string())) } SignRequest::ProxyEcdsa(SignProxyRequest { object_root, pubkey: ecdsa_key }) => { local_manager @@ -209,28 +205,36 @@ async fn handle_request_signature( .sign_proxy_ecdsa(&ecdsa_key, &object_root) .await .map(|sig| Json(sig).into_response()) - .map_err(|err| SignerModuleError::Internal(err.to_string())) } }, SigningManager::Dirk(dirk_manager) => match request { SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => dirk_manager .request_signature(pubkey, object_root) .await - .map(|sig| Json(sig).into_response()) - .map_err(|err| SignerModuleError::Internal(err.to_string())), + .map(|sig| Json(sig).into_response()), SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { dirk_manager .request_signature(bls_key, object_root) .await .map(|sig| Json(sig).into_response()) - .map_err(|err| SignerModuleError::Internal(err.to_string())) } SignRequest::ProxyEcdsa(_) => { - error!("ECDSA proxy sign request not supported with Dirk"); + error!( + event = "request_signature", + ?module_id, + ?req_id, + "ECDSA proxy sign request not supported with Dirk" + ); Err(SignerModuleError::DirkNotSupported) } }, + }; + + if let Err(err) = &res { + error!(event = "request_signature", ?module_id, ?req_id, "{err}"); } + + res } async fn handle_generate_proxy( @@ -240,35 +244,38 @@ async fn handle_generate_proxy( ) -> Result { let req_id = Uuid::new_v4(); - debug!(event = "generate_proxy", module_id=?module_id, ?req_id, "New request"); + debug!(event = "generate_proxy", ?module_id, scheme=?request.scheme, pubkey=%request.consensus_pubkey, ?req_id, "New request"); - match state.manager { + let res = match state.manager { SigningManager::Local(local_manager) => match request.scheme { EncryptionScheme::Bls => local_manager .write() .await - .create_proxy_bls(module_id, request.consensus_pubkey) + .create_proxy_bls(module_id.clone(), request.consensus_pubkey) .await - .map(|proxy_delegation| Json(proxy_delegation).into_response()) - .map_err(|err| SignerModuleError::Internal(err.to_string())), + .map(|proxy_delegation| Json(proxy_delegation).into_response()), EncryptionScheme::Ecdsa => local_manager .write() .await - .create_proxy_ecdsa(module_id, request.consensus_pubkey) + .create_proxy_ecdsa(module_id.clone(), request.consensus_pubkey) .await - .map(|proxy_delegation| Json(proxy_delegation).into_response()) - .map_err(|err| SignerModuleError::Internal(err.to_string())), + .map(|proxy_delegation| Json(proxy_delegation).into_response()), }, SigningManager::Dirk(dirk_manager) => match request.scheme { EncryptionScheme::Bls => dirk_manager - .generate_proxy_key(module_id, request.consensus_pubkey) + .generate_proxy_key(module_id.clone(), request.consensus_pubkey) .await - .map(|proxy_delegation| Json(proxy_delegation).into_response()) - .map_err(|err| SignerModuleError::Internal(err.to_string())), + .map(|proxy_delegation| Json(proxy_delegation).into_response()), EncryptionScheme::Ecdsa => { error!("ECDSA proxy generation not supported with Dirk"); Err(SignerModuleError::DirkNotSupported) } }, + }; + + if let Err(err) = &res { + error!(event = "generate_proxy", module_id=?module_id, ?req_id, "{err}"); } + + res } From 896585cc8d9b488deff9fd383e2c2060d0387437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 15 Jan 2025 15:08:11 -0300 Subject: [PATCH 042/104] Fix clippy --- crates/common/src/commit/request.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index 0d7157cc..eaeeb37d 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -85,19 +85,19 @@ impl Display for SignRequest { f, "Consensus(pubkey: {}, object_root: {})", req.pubkey, - hex::encode(&req.object_root) + hex::encode(req.object_root) ), SignRequest::ProxyBls(req) => write!( f, "BLS(pubkey: {}, object_root: {})", req.pubkey, - hex::encode(&req.object_root) + hex::encode(req.object_root) ), SignRequest::ProxyEcdsa(req) => write!( f, "ECDSA(pubkey: {}, object_root: {})", req.pubkey, - hex::encode(&req.object_root) + hex::encode(req.object_root) ), } } From ee100c8ce98fb536d246aa8d4f33ebf3eb3eefc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 15 Jan 2025 15:22:43 -0300 Subject: [PATCH 043/104] Add tracing logs for dirk requests --- crates/signer/src/manager/dirk.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 7b669476..b9f9c080 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -11,7 +11,7 @@ use cb_common::{ }; use rand::Rng; use tonic::transport::{Channel, ClientTlsConfig}; -use tracing::info; +use tracing::{info, trace}; use tree_hash::TreeHash; use crate::{ @@ -58,6 +58,8 @@ impl DirkManager { tls_config = tls_config.domain_name(server_domain); } + trace!(url=%config.url, "Stablishing connection with Dirk"); + let channel = Channel::from_shared(config.url.to_string()) .map_err(|_| eyre::eyre!("Invalid Dirk URL"))? .tls_config(tls_config) @@ -293,6 +295,8 @@ impl DirkManager { account: String, password: String, ) -> Result<(), SignerModuleError> { + trace!(account, "Sending AccountManager/Unlock request to Dirk"); + let mut client = AccountManagerClient::new(self.channel.clone()); let unlock_request = tonic::Request::new(UnlockAccountRequest { account: account.clone(), @@ -336,6 +340,8 @@ impl DirkManager { let account_name = format!("{consensus_account}/{module_id}/{uuid}"); let new_password = Self::random_password(); + trace!(account = account_name, "Sending AccountManager/Generate request to Dirk"); + let mut client = AccountManagerClient::new(self.channel.clone()); let generate_request = tonic::Request::new(GenerateRequest { account: account_name.clone(), @@ -383,6 +389,13 @@ impl DirkManager { ) -> Result { let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); + trace!( + %pubkey, + object_root = hex::encode(object_root), + domain = hex::encode(domain), + "Sending Signer/Sign request to Dirk" + ); + let mut signer_client = SignerClient::new(self.channel.clone()); let sign_request = tonic::Request::new(SignRequest { id: Some(SignerId::PublicKey(pubkey.to_vec())), @@ -410,6 +423,13 @@ impl DirkManager { ) .await?; + trace!( + %pubkey, + object_root = hex::encode(object_root), + domain = hex::encode(domain), + "Sending Signer/Sign request to Dirk" + ); + let sign_request = tonic::Request::new(SignRequest { id: Some(SignerId::PublicKey(pubkey.to_vec())), domain: domain.to_vec(), @@ -439,6 +459,8 @@ async fn get_accounts_in_wallets( channel: Channel, wallets: Vec, ) -> Result, SignerModuleError> { + trace!(?wallets, "Sending Lister/ListAccounts request to Dirk"); + let mut client = ListerClient::new(channel); let pubkeys_request = tonic::Request::new(ListAccountsRequest { paths: wallets }); let pubkeys_response = client From 42eb5ee4221998e9cd8f8cdc05d378a378d8523d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 15 Jan 2025 15:49:17 -0300 Subject: [PATCH 044/104] Update docs --- config.example.toml | 8 ++++++-- crates/common/src/signer/store.rs | 2 +- crates/signer/src/manager/dirk.rs | 4 ++-- docs/docs/get_started/configuration.md | 14 +++++++++++++- examples/configs/dirk_signer.toml | 5 ++++- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/config.example.toml b/config.example.toml index 65b614cf..82fc6642 100644 --- a/config.example.toml +++ b/config.example.toml @@ -150,8 +150,8 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # cert_path = "/path/to/client.crt" # Path to the client key # key_path = "/path/to/client.key" -# Wallets to use. Each wallet should have a `wallet/consensus` account -# wallets = ["wallet1", "wallet2"] +# Accounts to use as consensus keys. +# accounts = ["wallet1/accountX", "wallet2/accountY"] # Path to the secrets directory where the accounts passwords are stored # secrets_path = "/path/to/secrets" # Path to the CA certificate that signed the Dirk server certificate @@ -163,6 +163,10 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # Whether to try to unlock the accounts on sign failure # OPTIONAL, DEFAULT: false # unlock = false +# Configuration for how the Signer module should store proxy delegations. +# OPTIONAL +# [signer.dirk.store] +# proxy_dir = "/path/to/proxies" # For Local signer: # Configuration for how the Signer module should load validator keys. Currently two types of loaders are supported: # - File: load keys from a plain text file (unsafe, use only for testing purposes) diff --git a/crates/common/src/signer/store.rs b/crates/common/src/signer/store.rs index a5563019..a0052fa8 100644 --- a/crates/common/src/signer/store.rs +++ b/crates/common/src/signer/store.rs @@ -166,7 +166,7 @@ impl ProxyStore { .join("delegations") .join(module_id.to_string()) .join("bls") - .join(delegation.message.proxy.to_string()); + .join(format!("{}.sig", delegation.message.proxy)); let content = serde_json::to_vec(&delegation)?; if let Some(parent) = file_path.parent() { diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index b9f9c080..591ba48e 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -258,7 +258,7 @@ impl DirkManager { /// Read the password for an account from a file fn read_password(&self, account: String) -> Result { - fs::read_to_string(self.secrets_path.join(account.clone())).map_err(|err| { + fs::read_to_string(self.secrets_path.join(format!("{account}.pass"))).map_err(|err| { SignerModuleError::Internal(format!( "error reading password for account '{account}': {err}" )) @@ -283,7 +283,7 @@ impl DirkManager { fs::create_dir_all(account_dir.clone()).map_err(|err| { SignerModuleError::Internal(format!("error creating dir '{account_dir}': {err}")) })?; - fs::write(self.secrets_path.join(account.clone()), password).map_err(|err| { + fs::write(self.secrets_path.join(format!("{account}.pass")), password).map_err(|err| { SignerModuleError::Internal(format!( "error writing password for account '{account}': {err}" )) diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 36b61cc2..30477d2a 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -245,6 +245,7 @@ url = "https://remote.signer.url" Dirk is a distributed key management system that can be used to sign transactions. In this case the Signer module is needed as an intermediary between the modules and Dirk. The following parameters are needed: ```toml +[signer.dirk] url = "https://dirk.gateway.url" cert_path = "/path/to/client.crt" key_path = "/path/to/client.key" @@ -259,9 +260,20 @@ unlock = false - `cert_path` and `key_path` are the paths to the client certificate and key used to authenticate with Dirk. - `accounts` is a list of accounts that the Signer module will consider as the consensus keys. Each account has the format `/`. Accounts can be from different wallets. Generated proxy keys will have format `///`. -- `secrets_path` is the path to the folder containing the passwords of the accounts. Passwords must be in plain text in files with structure `//`. Generated proxy accounts secrets will be stored in `///`. +- `secrets_path` is the path to the folder containing the passwords of the accounts. Passwords must be in plain text in files with structure `//.pass`. Generated proxy accounts passwords will be stored in `////.pass`. - `unlock` is an optional parameter that can be set to `true` if you want to try to unlock the wallets on sign failure. Default is `false`. +Additionally, you can set a proxy store so that the delegation signatures for generated proxy keys are stored locally. As these signatures are not sensitive, the only supported store type is `File`: + +```toml +[signer.dirk.store] +proxy_dir = "/path/to/proxy_dir" +``` + +Delegation signatures will be stored in files with the format `/delegations//.sig`. + +A full example of a config file with Dirk can be found [here](https://github.com/Commit-Boost/commit-boost-client/blob/main/examples/configs/dirk_signer.toml). + ## Custom module We currently provide a test module that needs to be built locally. To build the module run: ```bash diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml index 039dca3e..12e39295 100644 --- a/examples/configs/dirk_signer.toml +++ b/examples/configs/dirk_signer.toml @@ -13,10 +13,13 @@ docker_image = "commitboost_signer" url = "https://gateway.dirk.url" cert_path = "/path/to/client.crt" key_path = "/path/to/client.key" -wallets = ["validator1", "validator2"] +accounts = ["wallet1/accountX", "wallet2/accountY"] secrets_path = "./dirk_secrets" unlock = true +[signer.dirk.store] +proxy_dir = "/path/to/proxies" + [[modules]] id = "DA_COMMIT" type = "commit" From 9efe61e6b027ffd35867b6c25c120e2b71caf7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 15 Jan 2025 15:53:54 -0300 Subject: [PATCH 045/104] Fix unsupported config --- crates/common/src/config/signer.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 20f9b19c..729a352f 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -130,6 +130,10 @@ impl StartSignerConfig { let ca_cert_path = load_env_var(DIRK_CA_CERT_ENV).map(PathBuf::from).ok().or(ca_cert_path); + if let Some(ProxyStore::ERC2335 { .. }) = store { + bail!("ERC2335 store is not supported with Dirk signer") + } + Ok(StartSignerConfig { chain: config.chain, server_port, From 047867be3b68a1043d940a4fcbb6b823529fda0c Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Mon, 10 Feb 2025 17:31:32 -0300 Subject: [PATCH 046/104] Dirk: Use a per-host configuration and save host info into a HashMap --- Cargo.lock | 1 + crates/common/src/config/signer.rs | 31 ++--- crates/signer/Cargo.toml | 1 + crates/signer/src/manager/dirk.rs | 204 ++++++++++++++++++++--------- 4 files changed, 157 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ba2af23..18d90a39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1428,6 +1428,7 @@ dependencies = [ "alloy", "axum 0.8.1", "axum-extra", + "base64 0.21.7", "bimap", "blst", "cb-common", diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 729a352f..b09ccd52 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -32,6 +32,17 @@ fn default_signer() -> String { SIGNER_IMAGE_DEFAULT.to_string() } +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub struct DirkHost { + /// Domain name of the server to use in TLS verification + pub domain: Option, + /// Complete URL of a Dirk gateway + pub url: Url, + /// Accounts used as consensus keys + pub accounts: Vec, +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub enum SignerType { @@ -49,20 +60,16 @@ pub enum SignerType { }, /// Dirk remote signer module Dirk { - /// Complete URL of a Dirk gateway - url: Url, + /// List of Dirk hosts with their accounts + hosts: Vec, /// Path to the client certificate cert_path: PathBuf, /// Path to the client key key_path: PathBuf, - /// Accounts used as consensus keys - accounts: Vec, /// Path to where the account passwords are stored secrets_path: PathBuf, /// Path to the CA certificate ca_cert_path: Option, - /// Domain name of the server to use in TLS verification - server_domain: Option, /// Whether to unlock the accounts in case they are locked #[serde(default)] unlock: bool, @@ -73,12 +80,10 @@ pub enum SignerType { #[derive(Clone, Debug)] pub struct DirkConfig { - pub url: Url, - pub accounts: Vec, + pub hosts: Vec, pub client_cert: Identity, pub secrets_path: PathBuf, pub cert_auth: Option, - pub server_domain: Option, pub unlock: bool, } @@ -112,13 +117,11 @@ impl StartSignerConfig { }), SignerType::Dirk { - url, + hosts, cert_path, key_path, - accounts, secrets_path, ca_cert_path, - server_domain, unlock, store, .. @@ -141,8 +144,7 @@ impl StartSignerConfig { loader: None, store, dirk: Some(DirkConfig { - url, - accounts, + hosts, client_cert: Identity::from_pem( std::fs::read_to_string(cert_path)?, std::fs::read_to_string(key_path)?, @@ -154,7 +156,6 @@ impl StartSignerConfig { } None => None, }, - server_domain, unlock, }), }) diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index a2a9ac7a..d4f467ae 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -40,6 +40,7 @@ bimap.workspace = true lazy_static.workspace = true derive_more.workspace = true rand.workspace = true +base64 = "0.21.7" [build-dependencies] tonic-build.workspace = true diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 591ba48e..4bad2e63 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -1,5 +1,6 @@ use std::{fs, path::PathBuf}; - +use std::collections::HashMap; +use std::fmt::{Debug}; use alloy::{hex, primitives::FixedBytes}; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, @@ -13,7 +14,6 @@ use rand::Rng; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::{info, trace}; use tree_hash::TreeHash; - use crate::{ error::SignerModuleError::{self, DirkCommunicationError}, proto::v1::{ @@ -22,14 +22,26 @@ use crate::{ GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, UnlockAccountRequest, }, }; +use crate::proto::v1::DistributedAccount; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; + +#[derive(Clone, Debug)] +enum WalletType { + Simple, + Distributed +} + #[derive(Clone, Debug)] struct Account { wallet: String, name: String, public_key: Option, + hosts: Vec, + wallet_type: WalletType, } + impl Account { pub fn complete_name(&self) -> String { format!("{}/{}", self.wallet, self.name) @@ -40,7 +52,7 @@ impl Account { pub struct DirkManager { chain: Chain, channel: Channel, - accounts: Vec, + accounts: HashMap, // host+pubkey -> account unlock: bool, secrets_path: PathBuf, proxy_store: Option, @@ -48,67 +60,116 @@ pub struct DirkManager { impl DirkManager { pub async fn new_from_config(chain: Chain, config: DirkConfig) -> eyre::Result { - let mut tls_config = ClientTlsConfig::new().identity(config.client_cert); + let mut tls_configs = Vec::new(); - if let Some(ca) = config.cert_auth { - tls_config = tls_config.ca_certificate(ca); - } + // Create a TLS config for each host + for host in config.hosts.clone() { + let mut tls_config = ClientTlsConfig::new().identity(config.client_cert.clone()); - if let Some(server_domain) = config.server_domain { - tls_config = tls_config.domain_name(server_domain); - } + if let Some(ca) = &config.cert_auth { + tls_config = tls_config.ca_certificate(ca.clone()); + trace!("Using custom CA certificate"); + } - trace!(url=%config.url, "Stablishing connection with Dirk"); + trace!(host.domain, "Using custom server domain"); + tls_config = tls_config.domain_name(host.domain.unwrap_or_default()); - let channel = Channel::from_shared(config.url.to_string()) - .map_err(|_| eyre::eyre!("Invalid Dirk URL"))? - .tls_config(tls_config) - .map_err(|_| eyre::eyre!("Invalid Dirk TLS config"))? - .connect() - .await - .map_err(|e| eyre::eyre!("Couldn't connect to Dirk: {e}"))?; + tls_configs.push(tls_config); + } - let dirk_accounts = get_accounts_in_wallets( - channel.clone(), - config - .accounts - .iter() - .filter_map(|account| Some(account.split_once("/")?.0.to_string())) - .collect(), - ) - .await?; - - let mut accounts = Vec::with_capacity(config.accounts.len()); - for account in config.accounts { - let (wallet, name) = account.split_once("/").ok_or(eyre::eyre!( - "Invalid account name: {account}. It must be in format wallet/account" - ))?; - let public_key = dirk_accounts.iter().find_map(|a| { - if a.name == account { - BlsPublicKey::try_from(a.public_key.as_slice()).ok() - } else { - None + // Create a channel for each host, attempt to connect, + // and save it into a hashmap Host->Channel + let mut channels = HashMap::new(); + for (i, tls_config) in tls_configs.iter().enumerate() { + let config_host = config.hosts[i].clone(); + let domain = config_host.domain.unwrap_or_default(); + match Channel::from_shared(config_host.url.to_string()) + .map_err(|_| eyre::eyre!("Invalid Dirk URL"))? + .tls_config(tls_config.clone()) + .map_err(|_| eyre::eyre!("Invalid Dirk TLS config"))? + .connect() + .await + { + Ok(ch) => { + channels.insert(domain, ch); } - }); - - accounts.push(Account { - wallet: wallet.to_string(), - name: name.to_string(), - public_key, - }); + Err(e) => { + trace!("Couldn't connect to Dirk with domain {}: {e}", domain); + } + } } - let wallets = - accounts.iter().map(|account| account.wallet.clone()).collect::>(); - let dirk_accounts = get_accounts_in_wallets(channel.clone(), wallets).await?; - for account in accounts.iter_mut() { - if let Some(dirk_account) = - dirk_accounts.iter().find(|a| a.name == account.complete_name()) - { - account.public_key = - Some(BlsPublicKey::try_from(dirk_account.public_key.as_slice())?); + + // TODO remove this, should use all channels later + let channel = channels + .values() + .next() + .ok_or_else(|| eyre::eyre!("Couldn't connect to Dirk with any of the domains"))? + .clone(); + + let mut accounts: HashMap = HashMap::new(); + + + for host in config.hosts { + let domain = host.domain.unwrap_or_default(); + let channel = channels.get(&domain).ok_or(eyre::eyre!( + "Couldn't connect to Dirk with domain {domain}" + ))?.clone(); + + let (dirk_accounts, dirk_distributed_accounts) = get_accounts_in_wallets( + channel.clone(), + host.accounts.iter() + .filter_map(|account| Some(account.split_once("/")?.0.to_string())) + .collect(), + ).await?; + + for account_name in host.accounts.clone() { + let (wallet, name) = account_name.split_once("/").ok_or(eyre::eyre!( + "Invalid account name: {account_name}. It must be in format wallet/account" + ))?; + + // Handle simple accounts + if let Some(dirk_account) = dirk_accounts.iter() + .find(|a| a.name == account_name) + { + let public_key = BlsPublicKey::try_from(dirk_account.public_key.as_slice())?; + let key = hex::encode(public_key); + + accounts.insert(key, Account { + wallet: wallet.to_string(), + name: name.to_string(), + public_key: Some(public_key), + hosts: vec![domain.clone()], + wallet_type: WalletType::Simple, + }); + } + + // Handle distributed accounts + if let Some(dist_account) = dirk_distributed_accounts.iter() + .find(|a| a.name == account_name) + { + let public_key = BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; + let key = hex::encode(public_key); + + accounts + .entry(key) + .and_modify(|account| { + if !account.hosts.contains(&domain) { + account.hosts.push(domain.clone()); + } + }) + .or_insert_with(|| Account { + wallet: wallet.to_string(), + name: name.to_string(), + public_key: Some(public_key), + hosts: vec![domain.clone()], + wallet_type: WalletType::Distributed, + }); + } } } + trace!(?accounts, "Accounts by host"); + Ok(Self { chain, channel, @@ -119,17 +180,26 @@ impl DirkManager { }) } + // TODO might be temporary, for testing + pub fn accounts(&self) -> Vec { + self.accounts.values().cloned().collect() + } + pub fn with_proxy_store(self, proxy_store: ProxyStore) -> eyre::Result { Ok(Self { proxy_store: Some(proxy_store), ..self }) } /// Get all available accounts in the `self.accounts` wallets async fn get_all_accounts(&self) -> Result, SignerModuleError> { - get_accounts_in_wallets( + let res = get_accounts_in_wallets( self.channel.clone(), - self.accounts.iter().map(|account| account.wallet.clone()).collect::>(), + self.accounts().iter().map(|account| account.wallet.clone()).collect::>(), ) - .await + .await; + match res { + Ok((accounts, _)) => Ok(accounts), + Err(err) => Err(err), + } } /// Get the complete account name (`wallet/account`) for a public key. @@ -140,7 +210,7 @@ impl DirkManager { pubkey: BlsPublicKey, ) -> Result, SignerModuleError> { match self - .accounts + .accounts() .iter() .find(|account| account.public_key.is_some_and(|account_pk| account_pk == pubkey)) { @@ -162,7 +232,7 @@ impl DirkManager { /// Returns the public keys of the config-registered accounts pub async fn consensus_pubkeys(&self) -> eyre::Result> { let registered_pubkeys = self - .accounts + .accounts() .iter() .filter_map(|account| account.public_key) .collect::>(); @@ -173,7 +243,7 @@ impl DirkManager { let accounts = self.get_all_accounts().await?; let expected_accounts: Vec = - self.accounts.iter().map(|account| account.complete_name()).collect(); + self.accounts().iter().map(|account| account.complete_name()).collect(); Ok(accounts .iter() @@ -198,7 +268,7 @@ impl DirkManager { Ok(accounts .iter() .filter_map(|account| { - if self.accounts.iter().any(|consensus_account| { + if self.accounts().iter().any(|consensus_account| { account.name.starts_with(&format!("{}/", consensus_account.complete_name())) }) { BlsPublicKey::try_from(account.public_key.as_slice()).ok() @@ -222,7 +292,7 @@ impl DirkManager { let mut proxy_maps = Vec::new(); - for consensus_account in self.accounts.iter() { + for consensus_account in self.accounts().iter() { let Some(consensus_key) = consensus_account.public_key else { continue; }; @@ -258,6 +328,8 @@ impl DirkManager { /// Read the password for an account from a file fn read_password(&self, account: String) -> Result { + let path = self.secrets_path.join(format!("{account}.pass")); + trace!(path = ?path, "Reading password from file"); fs::read_to_string(self.secrets_path.join(format!("{account}.pass"))).map_err(|err| { SignerModuleError::Internal(format!( "error reading password for account '{account}': {err}" @@ -328,7 +400,7 @@ impl DirkManager { .ok_or(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; if !self - .accounts + .accounts() .iter() .map(|account| account.complete_name()) .collect::>() @@ -458,7 +530,7 @@ impl DirkManager { async fn get_accounts_in_wallets( channel: Channel, wallets: Vec, -) -> Result, SignerModuleError> { +) -> Result<(Vec, Vec), SignerModuleError> { trace!(?wallets, "Sending Lister/ListAccounts request to Dirk"); let mut client = ListerClient::new(channel); @@ -472,5 +544,7 @@ async fn get_accounts_in_wallets( return Err(DirkCommunicationError("list accounts request returned error".to_string())); } - Ok(pubkeys_response.into_inner().accounts) + let inner = pubkeys_response.into_inner(); + Ok((inner.accounts, inner.distributed_accounts)) } + From a4768af09df89bfbaff26726ff95bd0deb07eddc Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Tue, 11 Feb 2025 15:57:41 -0300 Subject: [PATCH 047/104] Dirk: Use all channels and use the according one depending on the pubkey in the requests. Without support for Distributed wallets for now. --- crates/signer/src/manager/dirk.rs | 65 +++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 4bad2e63..4b8ac503 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -48,11 +48,12 @@ impl Account { } } + #[derive(Clone, Debug)] pub struct DirkManager { chain: Chain, - channel: Channel, - accounts: HashMap, // host+pubkey -> account + channels: HashMap, // domain -> channel + accounts: HashMap, // pubkey -> account unlock: bool, secrets_path: PathBuf, proxy_store: Option, @@ -172,7 +173,7 @@ impl DirkManager { Ok(Self { chain, - channel, + channels, accounts, unlock: config.unlock, secrets_path: config.secrets_path, @@ -190,16 +191,20 @@ impl DirkManager { } /// Get all available accounts in the `self.accounts` wallets - async fn get_all_accounts(&self) -> Result, SignerModuleError> { - let res = get_accounts_in_wallets( - self.channel.clone(), - self.accounts().iter().map(|account| account.wallet.clone()).collect::>(), - ) - .await; - match res { - Ok((accounts, _)) => Ok(accounts), - Err(err) => Err(err), + pub async fn get_all_accounts(&self) -> Result, SignerModuleError> { + let mut all_accounts = Vec::new(); + + // Query all channels and combine results + for channel in self.channels.values() { + let (accounts, _) = get_accounts_in_wallets( + channel.clone(), + self.accounts().iter().map(|account| account.wallet.clone()).collect(), + ) + .await?; + all_accounts.extend(accounts); } + + Ok(all_accounts) } /// Get the complete account name (`wallet/account`) for a public key. @@ -362,14 +367,40 @@ impl DirkManager { }) } + // Helper method to get channel for a public key + fn get_channel_for_pubkey(&self, pubkey: &BlsPublicKey) -> Result { + let key = hex::encode(pubkey); + let account = self.accounts.get(&key).ok_or_else(|| + SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()) + )?; + + // Get first available host's channel + let domain = account.hosts.first().ok_or_else(|| + SignerModuleError::Internal("Account has no associated hosts".to_string()) + )?; + + self.channels.get(domain).cloned().ok_or_else(|| + SignerModuleError::Internal(format!("No channel found for domain {}", domain)) + ) + } + async fn unlock_account( &self, account: String, password: String, ) -> Result<(), SignerModuleError> { - trace!(account, "Sending AccountManager/Unlock request to Dirk"); + // Find the public key associated with this account name + let account_entry = self.accounts.values().find(|a| a.complete_name() == account) + .ok_or_else(|| SignerModuleError::Internal(format!("Account not found: {}", account)))?; + + let channel = self.get_channel_for_pubkey( + account_entry.public_key.as_ref().ok_or_else(|| + SignerModuleError::Internal("Account has no public key".to_string()) + )? + )?; - let mut client = AccountManagerClient::new(self.channel.clone()); + trace!(account, "Sending AccountManager/Unlock request to Dirk"); + let mut client = AccountManagerClient::new(channel); let unlock_request = tonic::Request::new(UnlockAccountRequest { account: account.clone(), passphrase: password.as_bytes().to_vec(), @@ -392,6 +423,7 @@ impl DirkManager { module_id: ModuleId, consensus_pubkey: BlsPublicKey, ) -> Result, SignerModuleError> { + let channel = self.get_channel_for_pubkey(&consensus_pubkey)?; let uuid = uuid::Uuid::new_v4(); let consensus_account = self @@ -414,7 +446,7 @@ impl DirkManager { trace!(account = account_name, "Sending AccountManager/Generate request to Dirk"); - let mut client = AccountManagerClient::new(self.channel.clone()); + let mut client = AccountManagerClient::new(channel.clone()); let generate_request = tonic::Request::new(GenerateRequest { account: account_name.clone(), passphrase: new_password.as_bytes().to_vec(), @@ -459,6 +491,7 @@ impl DirkManager { pubkey: BlsPublicKey, object_root: [u8; 32], ) -> Result { + let channel = self.get_channel_for_pubkey(&pubkey)?; let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); trace!( @@ -468,7 +501,7 @@ impl DirkManager { "Sending Signer/Sign request to Dirk" ); - let mut signer_client = SignerClient::new(self.channel.clone()); + let mut signer_client = SignerClient::new(channel); let sign_request = tonic::Request::new(SignRequest { id: Some(SignerId::PublicKey(pubkey.to_vec())), domain: domain.to_vec(), From ecdb3452baff7ab743e69572c32caf91bc5a918b Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Tue, 11 Feb 2025 16:16:11 -0300 Subject: [PATCH 048/104] Dirk: Allow signing via Distributed Wallets (still not aggregating firms) --- crates/signer/src/manager/dirk.rs | 122 ++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 22 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 4b8ac503..8e7fdd65 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -31,14 +31,20 @@ enum WalletType { Distributed } +#[derive(Clone, Debug)] +struct HostInfo { + domain: String, + participant_id: u64, +} #[derive(Clone, Debug)] struct Account { wallet: String, name: String, public_key: Option, - hosts: Vec, + hosts: Vec, wallet_type: WalletType, + signing_threshold: u32, } @@ -139,8 +145,9 @@ impl DirkManager { wallet: wallet.to_string(), name: name.to_string(), public_key: Some(public_key), - hosts: vec![domain.clone()], + hosts: vec![HostInfo { domain: domain.clone(), participant_id: 1 }], wallet_type: WalletType::Simple, + signing_threshold: 1, }); } @@ -151,19 +158,32 @@ impl DirkManager { let public_key = BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; let key = hex::encode(public_key); + // Find the participant ID for this host from the participants list + let participant_id = dist_account.participants.iter() + .find(|p| p.name == domain) + .map(|p| p.id) + .ok_or_else(|| eyre::eyre!("Host {} not found in distributed account participants", domain))?; + accounts .entry(key) .and_modify(|account| { - if !account.hosts.contains(&domain) { - account.hosts.push(domain.clone()); + if !account.hosts.iter().any(|host| host.domain == domain) { + account.hosts.push(HostInfo { + domain: domain.clone(), + participant_id, + }); } }) .or_insert_with(|| Account { wallet: wallet.to_string(), name: name.to_string(), public_key: Some(public_key), - hosts: vec![domain.clone()], + hosts: vec![HostInfo { + domain: domain.clone(), + participant_id, + }], wallet_type: WalletType::Distributed, + signing_threshold: dist_account.signing_threshold, }); } } @@ -377,9 +397,9 @@ impl DirkManager { // Get first available host's channel let domain = account.hosts.first().ok_or_else(|| SignerModuleError::Internal("Account has no associated hosts".to_string()) - )?; + )?.domain.clone(); - self.channels.get(domain).cloned().ok_or_else(|| + self.channels.get(&domain).cloned().ok_or_else(|| SignerModuleError::Internal(format!("No channel found for domain {}", domain)) ) } @@ -491,8 +511,75 @@ impl DirkManager { pubkey: BlsPublicKey, object_root: [u8; 32], ) -> Result { - let channel = self.get_channel_for_pubkey(&pubkey)?; let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); + let key = hex::encode(pubkey); + let account = self.accounts.get(&key).ok_or_else(|| + SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()) + )?; + + match account.wallet_type { + WalletType::Simple => { + // Existing simple account logic + let channel = self.get_channel_for_pubkey(&pubkey)?; + self.sign_with_channel(channel, pubkey, account, domain, object_root).await + } + WalletType::Distributed => { + let mut signatures = Vec::new(); + let num_hosts_needed = account.signing_threshold as usize; + + if account.hosts.len() < num_hosts_needed { + return Err(SignerModuleError::Internal(format!( + "Not enough hosts available. Need {} but only have {}", + num_hosts_needed, + account.hosts.len() + ))); + } + + // Try to get signatures from enough hosts + for host in account.hosts.iter() { + let channel = self.channels.get(&host.domain).cloned().ok_or_else(|| + SignerModuleError::Internal(format!("No channel found for host {}", host.domain)) + )?; + + match self.sign_with_channel(channel, pubkey, account, domain, object_root).await { + Ok(signature) => signatures.push(signature), + Err(e) => { + trace!("Failed to get signature from host {}: {}", host.domain, e); + continue; + } + } + } + + trace!(?signatures, "Aggregating signatures"); + + if signatures.len() < num_hosts_needed { + return Err(SignerModuleError::Internal(format!( + "Could not collect enough signatures. Need {} but only got {}", + num_hosts_needed, + signatures.len() + ))); + } + + // TODO: Implement signature aggregation + // For now, just return the first signature + Ok(signatures[0]) + } + } + } + + // Helper method to sign with a specific channel + async fn sign_with_channel( + &self, + channel: Channel, + pubkey: BlsPublicKey, + account: &Account, + domain: [u8; 32], + object_root: [u8; 32], + ) -> Result { + let id = match account.wallet_type { + WalletType::Simple => SignerId::PublicKey(pubkey.to_vec()), + WalletType::Distributed => SignerId::Account(account.complete_name()), + }; trace!( %pubkey, @@ -501,9 +588,9 @@ impl DirkManager { "Sending Signer/Sign request to Dirk" ); - let mut signer_client = SignerClient::new(channel); + let mut signer_client = SignerClient::new(channel.clone()); let sign_request = tonic::Request::new(SignRequest { - id: Some(SignerId::PublicKey(pubkey.to_vec())), + id: Some(id.clone()), domain: domain.to_vec(), data: object_root.to_vec(), }); @@ -518,25 +605,16 @@ impl DirkManager { ResponseState::Denied if self.unlock => { info!("Failed to sign message, account {pubkey:#} may be locked. Unlocking and retrying."); - let account_name = self - .get_pubkey_account(pubkey) - .await? - .ok_or(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; + let account_name = account.complete_name(); + trace!(account = account_name, "Unlocking account"); self.unlock_account( account_name.clone(), self.read_password(account_name.clone())?, ) .await?; - trace!( - %pubkey, - object_root = hex::encode(object_root), - domain = hex::encode(domain), - "Sending Signer/Sign request to Dirk" - ); - let sign_request = tonic::Request::new(SignRequest { - id: Some(SignerId::PublicKey(pubkey.to_vec())), + id: Some(id), domain: domain.to_vec(), data: object_root.to_vec(), }); From 88746cad7a589ae17a88d43d03aac466ff730126 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Thu, 13 Feb 2025 15:14:34 -0300 Subject: [PATCH 049/104] Dirk: Aggregate distributed keys --- Cargo.lock | 183 ++++++++++++++++++++++++++++-- Cargo.toml | 1 + crates/signer/Cargo.toml | 2 + crates/signer/src/manager/dirk.rs | 92 +++++++++++++-- 4 files changed, 258 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18d90a39..436223d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -913,6 +913,12 @@ dependencies = [ "rand", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -1211,7 +1217,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1220,7 +1226,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1241,11 +1247,38 @@ dependencies = [ "zeroize", ] +[[package]] +name = "blsful" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a676ce0f93ae20ca6defc223b5ac459a6f071bd88c03100a14cb93604a5e994c" +dependencies = [ + "anyhow", + "arrayref", + "blstrs_plus", + "hex", + "hkdf", + "merlin", + "pairing", + "rand", + "rand_chacha", + "rand_core", + "serde", + "serde_bare", + "sha2 0.10.8", + "sha3", + "subtle", + "thiserror 1.0.69", + "uint-zigzag", + "vsss-rs", + "zeroize", +] + [[package]] name = "blst" -version = "0.3.13" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874" +checksum = "62dc83a094a71d43eeadd254b1ec2d24cb6a0bb6cadce00df51f0db594711a32" dependencies = [ "cc", "glob", @@ -1253,6 +1286,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "blstrs_plus" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a16dd4b0d6b4538e1fa0388843acb186363082713a8fc8416d802a04d013818" +dependencies = [ + "arrayref", + "blst", + "elliptic-curve", + "ff", + "group", + "pairing", + "rand_core", + "serde", + "subtle", + "zeroize", +] + [[package]] name = "builder_log" version = "0.5.0" @@ -1430,6 +1481,7 @@ dependencies = [ "axum-extra", "base64 0.21.7", "bimap", + "blsful", "blst", "cb-common", "cb-metrics", @@ -1438,6 +1490,7 @@ dependencies = [ "headers", "k256", "lazy_static", + "log", "prometheus", "prost", "rand", @@ -1499,7 +1552,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1650,6 +1703,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.16" @@ -1708,7 +1770,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core", "subtle", "zeroize", @@ -1720,7 +1782,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.7", "typenum", ] @@ -1730,7 +1792,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ - "generic-array", + "generic-array 0.14.7", "subtle", ] @@ -1935,7 +1997,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -2021,8 +2083,9 @@ dependencies = [ "crypto-bigint", "digest 0.10.7", "ff", - "generic-array", + "generic-array 0.14.7", "group", + "hkdf", "pkcs8", "rand_core", "sec1", @@ -2258,6 +2321,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ + "bitvec", "rand_core", "subtle", ] @@ -2428,6 +2492,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic-array" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c8444bc9d71b935156cc0ccab7f622180808af7867b1daae6547d773591703" +dependencies = [ + "typenum", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -2458,7 +2531,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", + "rand", "rand_core", + "rand_xorshift", "subtle", ] @@ -2565,6 +2640,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.11.0" @@ -2932,7 +3016,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -3114,6 +3198,18 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + [[package]] name = "mime" version = "0.3.17" @@ -3357,6 +3453,15 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + [[package]] name = "parity-scale-codec" version = "3.6.12" @@ -4104,7 +4209,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", - "generic-array", + "generic-array 0.14.7", "pkcs8", "subtle", "zeroize", @@ -4172,6 +4277,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bare" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c55386eed0f1ae957b091dc2ca8122f287b60c79c774cbe3d5f2b69fded660" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.217" @@ -4632,6 +4746,26 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "thiserror-impl-no-std" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e6318948b519ba6dc2b442a6d0b904ebfb8d411a3ad3e07843615a72249758" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "thiserror-no-std" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ad459d94dd517257cc96add8a43190ee620011bb6e6cdc82dafd97dfafafea" +dependencies = [ + "thiserror-impl-no-std", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -5115,6 +5249,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "uint-zigzag" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abbf77aed65cb885a8ba07138c365879be3d9a93dce82bf6cc50feca9138ec15" +dependencies = [ + "core2", +] + [[package]] name = "unarray" version = "0.1.4" @@ -5241,6 +5384,22 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsss-rs" +version = "4.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fabeca519a296f0b39428cfe496b600c0179c9498687986449d61fa40e60806" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "generic-array 1.2.0", + "rand_core", + "serde", + "sha3", + "subtle", + "thiserror-no-std", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index cd1fd61a..19f21b9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ prometheus = "0.13.4" # crypto blst = "0.3.11" +blsful = "2.5" tree_hash = "0.8" tree_hash_derive = "0.8" eth2_keystore = { git = "https://github.com/sigp/lighthouse", rev = "9e12c21f268c80a3f002ae0ca27477f9f512eb6f" } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index d4f467ae..d058a946 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -28,6 +28,7 @@ prometheus.workspace = true # crypto blst.workspace = true +blsful.workspace = true tree_hash.workspace = true tree_hash_derive.workspace = true k256.workspace = true @@ -41,6 +42,7 @@ lazy_static.workspace = true derive_more.workspace = true rand.workspace = true base64 = "0.21.7" +log = "0.4.25" [build-dependencies] tonic-build.workspace = true diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 8e7fdd65..ab5c9a65 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -2,6 +2,8 @@ use std::{fs, path::PathBuf}; use std::collections::HashMap; use std::fmt::{Debug}; use alloy::{hex, primitives::FixedBytes}; +use alloy::rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN; +use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, config::DirkConfig, @@ -525,6 +527,7 @@ impl DirkManager { } WalletType::Distributed => { let mut signatures = Vec::new(); + let mut identifiers = Vec::new(); let num_hosts_needed = account.signing_threshold as usize; if account.hosts.len() < num_hosts_needed { @@ -535,14 +538,26 @@ impl DirkManager { ))); } - // Try to get signatures from enough hosts - for host in account.hosts.iter() { + // Try to get signatures from hosts + for host in &account.hosts { + if signatures.len() >= num_hosts_needed { + break; + } + let channel = self.channels.get(&host.domain).cloned().ok_or_else(|| SignerModuleError::Internal(format!("No channel found for host {}", host.domain)) )?; match self.sign_with_channel(channel, pubkey, account, domain, object_root).await { - Ok(signature) => signatures.push(signature), + Ok(signature) => { + signatures.push(signature); + identifiers.push(host.participant_id); + trace!( + host = host.domain, + participant_id = host.participant_id, + "Got signature shard" + ); + }, Err(e) => { trace!("Failed to get signature from host {}: {}", host.domain, e); continue; @@ -550,8 +565,6 @@ impl DirkManager { } } - trace!(?signatures, "Aggregating signatures"); - if signatures.len() < num_hosts_needed { return Err(SignerModuleError::Internal(format!( "Could not collect enough signatures. Need {} but only got {}", @@ -560,9 +573,15 @@ impl DirkManager { ))); } - // TODO: Implement signature aggregation - // For now, just return the first signature - Ok(signatures[0]) + trace!( + num_shards = signatures.len(), + ?identifiers, + "Recovering master signature from shards" + ); + + aggregate_partial_signatures(&signatures, &identifiers) + .ok_or_else(|| SignerModuleError::Internal( + "Failed to recover master signature from shards".to_string())) } } } @@ -659,3 +678,60 @@ async fn get_accounts_in_wallets( Ok((inner.accounts, inner.distributed_accounts)) } + +pub fn aggregate_partial_signatures( + partials: &[BlsSignature], + identifiers: &[u64], +) -> Option { + // Ensure the number of partial signatures matches the number of identifiers + if partials.len() != identifiers.len() { + trace!("aggregate_partial_signatures: Invalid number of partial signatures"); + return None; + } + + // Deserialize partial signatures into G2 points + let mut points = Vec::new(); + for sig in partials { + if sig.len() != BLS_SIGNATURE_BYTES_LEN { + trace!("aggregate_partial_signatures: Invalid signature length"); + return None; + } + let arr: [u8; BLS_SIGNATURE_BYTES_LEN] = (*sig).into(); + let opt: Option = G2Affine::from_compressed(&arr).into(); + let opt: Option = G2Projective::from(&opt.unwrap()).into(); + if let Some(point) = opt { + points.push(point); + } else { + trace!("aggregate_partial_signatures: Failed to deserialize signature"); + return None; + } + } + + // Create a map of identifiers to their corresponding points + let mut shares: HashMap = HashMap::new(); + for (id, point) in identifiers.iter().zip(points.iter()) { + shares.insert(*id, point); + } + + // Perform Lagrange interpolation to recover the master signature + let mut recovered = G2Projective::identity(); + for (id, point) in &shares { + // Compute the Lagrange coefficient for this identifier + let mut numerator = Scalar::from(1u32); + let mut denominator = Scalar::from(1u32); + for (other_id, _) in &shares { + if other_id != id { + numerator *= Scalar::from(*other_id); + denominator *= Scalar::from(*other_id) - Scalar::from(*id); + } + } + let lagrange_coeff = numerator * denominator.invert().unwrap(); + + // Multiply the point by the Lagrange coefficient and add to the recovered point + recovered += **point * lagrange_coeff; + } + + // Serialize the recovered point back into a BlsSignature + let bytes = recovered.to_compressed(); + Some(bytes.into()) +} From e701b20ee4668d61fc94c2d25bd8ccd0a3cfcf0e Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Wed, 12 Feb 2025 12:07:00 -0300 Subject: [PATCH 050/104] Dirk: Handle Proxy multisig --- crates/common/src/signer/store.rs | 3 +- crates/signer/src/manager/dirk.rs | 375 +++++++++++++++++++++--------- crates/signer/src/service.rs | 2 +- 3 files changed, 274 insertions(+), 106 deletions(-) diff --git a/crates/common/src/signer/store.rs b/crates/common/src/signer/store.rs index a0052fa8..ba98fba8 100644 --- a/crates/common/src/signer/store.rs +++ b/crates/common/src/signer/store.rs @@ -22,7 +22,7 @@ use eth2_keystore::{ use eyre::OptionExt; use rand::Rng; use serde::{Deserialize, Serialize}; -use tracing::warn; +use tracing::{trace, warn}; use super::{load_bls_signer, load_ecdsa_signer}; use crate::{ @@ -168,6 +168,7 @@ impl ProxyStore { .join("bls") .join(format!("{}.sig", delegation.message.proxy)); let content = serde_json::to_vec(&delegation)?; + trace!(?content, "Writing BLS delegation to {file_path:?}"); if let Some(parent) = file_path.parent() { create_dir_all(parent)?; diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index ab5c9a65..8fdc32d3 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -1,5 +1,5 @@ use std::{fs, path::PathBuf}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::{Debug}; use alloy::{hex, primitives::FixedBytes}; use alloy::rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN; @@ -14,7 +14,7 @@ use cb_common::{ }; use rand::Rng; use tonic::transport::{Channel, ClientTlsConfig}; -use tracing::{info, trace}; +use tracing::{info, trace, warn}; use tree_hash::TreeHash; use crate::{ error::SignerModuleError::{self, DirkCommunicationError}, @@ -25,7 +25,6 @@ use crate::{ }, }; use crate::proto::v1::DistributedAccount; -use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; #[derive(Clone, Debug)] enum WalletType { @@ -47,6 +46,7 @@ struct Account { hosts: Vec, wallet_type: WalletType, signing_threshold: u32, + is_proxy: bool, } @@ -108,16 +108,8 @@ impl DirkManager { } } - // TODO remove this, should use all channels later - let channel = channels - .values() - .next() - .ok_or_else(|| eyre::eyre!("Couldn't connect to Dirk with any of the domains"))? - .clone(); - let mut accounts: HashMap = HashMap::new(); - for host in config.hosts { let domain = host.domain.unwrap_or_default(); let channel = channels.get(&domain).ok_or(eyre::eyre!( @@ -132,33 +124,42 @@ impl DirkManager { ).await?; for account_name in host.accounts.clone() { - let (wallet, name) = account_name.split_once("/").ok_or(eyre::eyre!( + let (wallet, _) = account_name.split_once("/").ok_or(eyre::eyre!( "Invalid account name: {account_name}. It must be in format wallet/account" ))?; // Handle simple accounts - if let Some(dirk_account) = dirk_accounts.iter() - .find(|a| a.name == account_name) + for dirk_account in dirk_accounts.iter() + .filter(|a| { + a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) + }) { let public_key = BlsPublicKey::try_from(dirk_account.public_key.as_slice())?; - let key = hex::encode(public_key); - - accounts.insert(key, Account { + let key_name = dirk_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + trace!(?dirk_account.name, ?key_name, "Adding account to hashmap"); + let is_proxy = key_name.contains('/'); + + accounts.insert(hex::encode(public_key), Account { wallet: wallet.to_string(), - name: name.to_string(), + name: key_name.to_string(), public_key: Some(public_key), hosts: vec![HostInfo { domain: domain.clone(), participant_id: 1 }], wallet_type: WalletType::Simple, signing_threshold: 1, + is_proxy, }); } // Handle distributed accounts - if let Some(dist_account) = dirk_distributed_accounts.iter() - .find(|a| a.name == account_name) + for dist_account in dirk_distributed_accounts.iter() + .filter(|a| { + a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) + }) { let public_key = BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; - let key = hex::encode(public_key); + let key_name = dist_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + let is_proxy = key_name.contains('/'); + trace!(?dist_account.name, ?key_name, "Adding distributed account to hashmap"); // Find the participant ID for this host from the participants list let participant_id = dist_account.participants.iter() @@ -167,7 +168,7 @@ impl DirkManager { .ok_or_else(|| eyre::eyre!("Host {} not found in distributed account participants", domain))?; accounts - .entry(key) + .entry(hex::encode(public_key)) .and_modify(|account| { if !account.hosts.iter().any(|host| host.domain == domain) { account.hosts.push(HostInfo { @@ -178,7 +179,7 @@ impl DirkManager { }) .or_insert_with(|| Account { wallet: wallet.to_string(), - name: name.to_string(), + name: key_name.to_string(), public_key: Some(public_key), hosts: vec![HostInfo { domain: domain.clone(), @@ -186,6 +187,7 @@ impl DirkManager { }], wallet_type: WalletType::Distributed, signing_threshold: dist_account.signing_threshold, + is_proxy, }); } } @@ -203,30 +205,52 @@ impl DirkManager { }) } - // TODO might be temporary, for testing - pub fn accounts(&self) -> Vec { + fn accounts(&self) -> Vec { self.accounts.values().cloned().collect() } + fn accounts_non_proxy(&self) -> Vec { + self.accounts.values().filter(|a| !a.is_proxy).cloned().collect() + } + pub fn with_proxy_store(self, proxy_store: ProxyStore) -> eyre::Result { Ok(Self { proxy_store: Some(proxy_store), ..self }) } /// Get all available accounts in the `self.accounts` wallets pub async fn get_all_accounts(&self) -> Result, SignerModuleError> { - let mut all_accounts = Vec::new(); - + trace!("CALLED"); + let mut seen_accounts: HashSet> = HashSet::new(); + let mut unique_accounts = Vec::new(); + // Query all channels and combine results for channel in self.channels.values() { - let (accounts, _) = get_accounts_in_wallets( + let (accounts, distributed_accounts) = get_accounts_in_wallets( channel.clone(), - self.accounts().iter().map(|account| account.wallet.clone()).collect(), + self.accounts_non_proxy().iter().map(|account| account.wallet.clone()).collect(), ) .await?; - all_accounts.extend(accounts); + + // There might be duplicates since we're querying several hosts, + // so only keep unique ones + for account in accounts { + if seen_accounts.insert(account.public_key.clone()) { + unique_accounts.push(account); + } + } + // TODO perhaps this is not what we want + for account in distributed_accounts { + if seen_accounts.insert(account.composite_public_key.clone()) { + unique_accounts.push(DirkAccount { + name: account.name, + public_key: account.composite_public_key, + uuid: account.uuid, + }); + } + } } - - Ok(all_accounts) + + Ok(unique_accounts) } /// Get the complete account name (`wallet/account`) for a public key. @@ -259,7 +283,7 @@ impl DirkManager { /// Returns the public keys of the config-registered accounts pub async fn consensus_pubkeys(&self) -> eyre::Result> { let registered_pubkeys = self - .accounts() + .accounts_non_proxy() .iter() .filter_map(|account| account.public_key) .collect::>(); @@ -319,7 +343,7 @@ impl DirkManager { let mut proxy_maps = Vec::new(); - for consensus_account in self.accounts().iter() { + for consensus_account in self.accounts_non_proxy().iter() { let Some(consensus_key) = consensus_account.public_key else { continue; }; @@ -389,123 +413,179 @@ impl DirkManager { }) } - // Helper method to get channel for a public key + /// Get the associated channel for a public key fn get_channel_for_pubkey(&self, pubkey: &BlsPublicKey) -> Result { + trace!(%pubkey, "Getting channel for public key"); let key = hex::encode(pubkey); let account = self.accounts.get(&key).ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()) )?; - // Get first available host's channel - let domain = account.hosts.first().ok_or_else(|| - SignerModuleError::Internal("Account has no associated hosts".to_string()) - )?.domain.clone(); + // Try to find any available host's channel + for host in &account.hosts { + if let Some(channel) = self.channels.get(&host.domain) { + return Ok(channel.clone()); + } + } - self.channels.get(&domain).cloned().ok_or_else(|| - SignerModuleError::Internal(format!("No channel found for domain {}", domain)) - ) + // If no channel is found, return an error + Err(SignerModuleError::Internal( + "No available channel found for any host".to_string() + )) } + /// Unlock an account. For distributed accounts this is done for all it's hosts. async fn unlock_account( &self, account: String, password: String, ) -> Result<(), SignerModuleError> { - // Find the public key associated with this account name - let account_entry = self.accounts.values().find(|a| a.complete_name() == account) + let account_entry = self.accounts.values() + .find(|a| a.complete_name() == account) .ok_or_else(|| SignerModuleError::Internal(format!("Account not found: {}", account)))?; - let channel = self.get_channel_for_pubkey( - account_entry.public_key.as_ref().ok_or_else(|| - SignerModuleError::Internal("Account has no public key".to_string()) - )? - )?; + match account_entry.wallet_type { + WalletType::Distributed => { + // For distributed accounts, unlock on all hosts + for host in &account_entry.hosts { + if let Some(channel) = self.channels.get(&host.domain) { + self.unlock_account_on_channel(channel, &account, &password, Some(&host.domain)).await?; + } + } + } + WalletType::Simple => { + // For simple accounts, unlock on a single host + let channel = self.get_channel_for_pubkey( + account_entry.public_key.as_ref().ok_or_else(|| + SignerModuleError::Internal("Account has no public key".to_string()) + )? + )?; + self.unlock_account_on_channel(&channel, &account, &password, None).await?; + } + } + Ok(()) + } + + /// Unlock an account on a specific channel + async fn unlock_account_on_channel( + &self, + channel: &Channel, + account: &str, + password: &str, + host_domain: Option<&str>, + ) -> Result<(), SignerModuleError> { + trace!(account, host = host_domain, "unlock_account_on_channel"); - trace!(account, "Sending AccountManager/Unlock request to Dirk"); - let mut client = AccountManagerClient::new(channel); + let mut client = AccountManagerClient::new(channel.clone()); let unlock_request = tonic::Request::new(UnlockAccountRequest { - account: account.clone(), + account: account.to_string(), passphrase: password.as_bytes().to_vec(), }); let unlock_response = client.unlock(unlock_request).await.map_err(|err| { - DirkCommunicationError(format!("error unlocking account '{account}': {err}")) + DirkCommunicationError( + format!("unlock_account_on_channel error unlocking account: {err}") + ) })?; + if unlock_response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError(format!( - "unlock request for '{account}' returned error" - ))); + let err = unlock_response.get_ref(); + warn!(?err, "unlock_account_on_channel error response"); + return Err( + DirkCommunicationError("unlock_account_on_channel error response received".to_string()) + ); } Ok(()) } pub async fn generate_proxy_key( - &self, + &mut self, module_id: ModuleId, consensus_pubkey: BlsPublicKey, ) -> Result, SignerModuleError> { - let channel = self.get_channel_for_pubkey(&consensus_pubkey)?; - let uuid = uuid::Uuid::new_v4(); - let consensus_account = self .get_pubkey_account(consensus_pubkey) .await? .ok_or(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; - if !self - .accounts() - .iter() - .map(|account| account.complete_name()) - .collect::>() - .contains(&consensus_account) - { - return Err(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; - } + let consensus_account_info = self.accounts() + .into_iter() + .find(|account| account.complete_name() == consensus_account) + .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; + let uuid = uuid::Uuid::new_v4(); let account_name = format!("{consensus_account}/{module_id}/{uuid}"); let new_password = Self::random_password(); - trace!(account = account_name, "Sending AccountManager/Generate request to Dirk"); - - let mut client = AccountManagerClient::new(channel.clone()); - let generate_request = tonic::Request::new(GenerateRequest { - account: account_name.clone(), - passphrase: new_password.as_bytes().to_vec(), - participants: 1, - signing_threshold: 1, - }); + match consensus_account_info.wallet_type { + WalletType::Simple => { + let channel = self.get_channel_for_pubkey(&consensus_pubkey)?; + self.generate_simple_proxy(channel, account_name, new_password, consensus_pubkey, module_id).await + } + WalletType::Distributed => { + // Pick the first available host to generate the key + let host = consensus_account_info.hosts.first().ok_or_else(|| + SignerModuleError::Internal("No hosts available for consensus account".to_string()) + )?; + + let channel = self.channels.get(&host.domain).cloned().ok_or_else(|| + SignerModuleError::Internal(format!("No channel found for host {}", host.domain)) + )?; + + let mut client = AccountManagerClient::new(channel.clone()); + let generate_request = tonic::Request::new(GenerateRequest { + account: account_name.clone(), + passphrase: new_password.as_bytes().to_vec(), + participants: consensus_account_info.hosts.len() as u32, + signing_threshold: consensus_account_info.signing_threshold, + }); - let generate_response = client - .generate(generate_request) - .await - .map_err(|err| DirkCommunicationError(format!("error on generate request: {err}")))?; + trace!(host = host.domain, "Sending generate request for distributed proxy key"); + let response = client.generate(generate_request).await + .map_err(|e| DirkCommunicationError(format!("Failed to generate proxy key: {e}")))?; - if generate_response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError("generate request returned error".to_string())); - } - - self.store_password(account_name.clone(), new_password.clone())?; + if response.get_ref().state() != ResponseState::Succeeded { + return Err(DirkCommunicationError("generate request returned error".to_string())); + } - let proxy_key = - BlsPublicKey::try_from(generate_response.into_inner().public_key.as_slice()).map_err( - |_| DirkCommunicationError("return value is not a valid public key".to_string()), - )?; + self.store_password(account_name.clone(), new_password.clone())?; + + trace!(?response, "Generated new proxy key"); + let proxy_key = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) + .map_err(|_| DirkCommunicationError("Invalid proxy public key".to_string()))?; + + // Add the new proxy account to our accounts map + let consensus_name = consensus_account_info.name; + let hashmap_key = hex::encode(&proxy_key); + self.accounts.insert(hashmap_key.clone(), Account { + wallet: consensus_account_info.wallet.clone(), + name: format!("{consensus_name}/{module_id}/{uuid}"), + public_key: Some(proxy_key), + hosts: consensus_account_info.hosts.clone(), + wallet_type: WalletType::Distributed, + signing_threshold: consensus_account_info.signing_threshold, + is_proxy: true, + }); + let added = self.accounts.get(&hashmap_key).ok_or(SignerModuleError::Internal( + "Failed to add new proxy account to accounts map".to_string() + ))?; + trace!(?hashmap_key, ?added, "Proxy account added"); - self.unlock_account(account_name, new_password).await?; + // Get delegation signature from the consensus account + let message = ProxyDelegation { delegator: consensus_pubkey, proxy: proxy_key }; + let signature = self.request_signature(consensus_pubkey, message.tree_hash_root().0).await?; + let delegation = SignedProxyDelegation { message, signature }; - let message = ProxyDelegation { delegator: consensus_pubkey, proxy: proxy_key }; - let signature = - self.request_signature(consensus_pubkey, message.tree_hash_root().0).await?; - let delegation = SignedProxyDelegation { message, signature }; + if let Some(store) = &self.proxy_store { + store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { + SignerModuleError::Internal(format!("error storing delegation signature: {err}")) + })?; + } - if let Some(store) = &self.proxy_store { - store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { - SignerModuleError::Internal(format!("error storing delegation signature: {err}")) - })?; + Ok(delegation) + } } - - Ok(delegation) } pub async fn request_signature( @@ -548,6 +628,7 @@ impl DirkManager { SignerModuleError::Internal(format!("No channel found for host {}", host.domain)) )?; + trace!(host = host.domain, "Requesting signature shard for creating proxy"); match self.sign_with_channel(channel, pubkey, account, domain, object_root).await { Ok(signature) => { signatures.push(signature); @@ -595,6 +676,7 @@ impl DirkManager { domain: [u8; 32], object_root: [u8; 32], ) -> Result { + trace!(?account, "sign_with_channel"); let id = match account.wallet_type { WalletType::Simple => SignerId::PublicKey(pubkey.to_vec()), WalletType::Distributed => SignerId::Account(account.complete_name()), @@ -602,8 +684,9 @@ impl DirkManager { trace!( %pubkey, - object_root = hex::encode(object_root), - domain = hex::encode(domain), + ?id, + is_proxy = account.is_proxy, + wallet_type = ?account.wallet_type, "Sending Signer/Sign request to Dirk" ); @@ -654,6 +737,90 @@ impl DirkManager { )?, )) } + + async fn generate_simple_proxy( + &mut self, + channel: Channel, + account_name: String, + new_password: String, + consensus_pubkey: BlsPublicKey, + module_id: ModuleId, + ) -> Result, SignerModuleError> { + // Generate the new proxy key + let mut client = AccountManagerClient::new(channel.clone()); + let generate_request = tonic::Request::new(GenerateRequest { + account: account_name.clone(), + passphrase: new_password.as_bytes().to_vec(), + participants: 1, + signing_threshold: 1, + }); + + trace!(account = account_name, "Sending AccountManager/Generate request to Dirk"); + let response = client + .generate(generate_request) + .await + .map_err(|err| DirkCommunicationError(format!("error on generate request: {err}")))?; + + if response.get_ref().state() != ResponseState::Succeeded { + return Err(DirkCommunicationError("generate request returned error".to_string())); + } + + // Store the password for future use + self.store_password(account_name.clone(), new_password.clone())?; + + // Get the proxy public key from the response + let proxy_key = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) + .map_err(|_| DirkCommunicationError("return value is not a valid public key".to_string()))?; + + // Get the consensus account info to copy host information + let consensus_account = self.accounts.get(&hex::encode(&consensus_pubkey)) + .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; + + let first_host = consensus_account.hosts.first() + .ok_or_else(|| SignerModuleError::Internal("Consensus account has no associated hosts".to_string()))?; + + // Store the new proxy account in our accounts map + let hashmap_key = hex::encode(&proxy_key); + // Remove the wallet part from the name + let account_name_without_wallet = String::from(account_name.split_once("/") + .map(|(_, n)| n).unwrap_or_default()); + self.accounts.insert(hashmap_key.clone(), Account { + wallet: consensus_account.wallet.clone(), + name: account_name_without_wallet.clone(), + public_key: Some(proxy_key), + hosts: vec![HostInfo { + domain: first_host.domain.clone(), + participant_id: first_host.participant_id, + }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy: true, + }); + let added = self.accounts.get(&hashmap_key).ok_or(SignerModuleError::Internal( + "Failed to add new proxy account to accounts map".to_string() + ))?; + trace!(?hashmap_key, ?added, "Account added"); + + // Unlock the account for immediate use + self.unlock_account(account_name, new_password).await?; + + // Get signature for the delegation from the consensus account + let message = ProxyDelegation { + delegator: consensus_pubkey, + proxy: proxy_key, + }; + let signature = self.request_signature(consensus_pubkey, message.tree_hash_root().0).await?; + let delegation = SignedProxyDelegation { message, signature }; + + // Store the delegation if we have a proxy store configured + if let Some(store) = &self.proxy_store { + store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { + SignerModuleError::Internal(format!("error storing delegation signature: {err}")) + })?; + } + + Ok(delegation) + } } /// Get the accounts for the wallets passed as argument diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 1d03b108..d72a521a 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -261,7 +261,7 @@ async fn handle_generate_proxy( .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), }, - SigningManager::Dirk(dirk_manager) => match request.scheme { + SigningManager::Dirk(mut dirk_manager) => match request.scheme { EncryptionScheme::Bls => dirk_manager .generate_proxy_key(module_id.clone(), request.consensus_pubkey) .await From d09b360736aa5fcdd4a368c6d40c796e8113a317 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Fri, 14 Feb 2025 12:37:46 -0300 Subject: [PATCH 051/104] Dirk: Wrap DirkManager with an Arc. This fixes the bug where if we created a proxy key we couldn't use it to sign unless we closed the client and opened again. This was happening because the DirkManager was getting cloned in each request thus the hashmap was not modified in further requests. --- crates/signer/src/manager/mod.rs | 8 ++++---- crates/signer/src/service.rs | 28 +++++++++++++++++----------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index 959edfc1..dabb25b1 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -13,7 +13,7 @@ pub mod local; #[derive(Clone)] pub enum SigningManager { Local(Arc>), - Dirk(DirkManager), + Dirk(Arc>), } impl SigningManager { @@ -23,7 +23,7 @@ impl SigningManager { SigningManager::Local(local_manager) => { Ok(local_manager.read().await.consensus_pubkeys().len()) } - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.consensus_pubkeys().await?.len()), + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.read().await.consensus_pubkeys().await?.len()), } } @@ -35,7 +35,7 @@ impl SigningManager { let proxies = manager.proxies(); Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) } - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.proxies().await?.len()), + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.read().await.proxies().await?.len()), } } @@ -48,7 +48,7 @@ impl SigningManager { local_manager.read().await.get_consensus_proxy_maps(module_id) } SigningManager::Dirk(dirk_manager) => { - dirk_manager.get_consensus_proxy_maps(module_id).await + dirk_manager.read().await.get_consensus_proxy_maps(module_id).await } } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index d72a521a..e325891c 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -66,7 +66,7 @@ impl SigningService { } SigningState { - manager: SigningManager::Dirk(dirk_manager), + manager: SigningManager::Dirk(Arc::new(RwLock::new(dirk_manager))), jwts: config.jwts.into(), } } @@ -208,12 +208,16 @@ async fn handle_request_signature( } }, SigningManager::Dirk(dirk_manager) => match request { - SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => dirk_manager - .request_signature(pubkey, object_root) - .await - .map(|sig| Json(sig).into_response()), + SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => { + let manager = dirk_manager.write().await; + manager + .request_signature(pubkey, object_root) + .await + .map(|sig| Json(sig).into_response()) + }, SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { - dirk_manager + let manager = dirk_manager.write().await; + manager .request_signature(bls_key, object_root) .await .map(|sig| Json(sig).into_response()) @@ -261,11 +265,13 @@ async fn handle_generate_proxy( .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), }, - SigningManager::Dirk(mut dirk_manager) => match request.scheme { - EncryptionScheme::Bls => dirk_manager - .generate_proxy_key(module_id.clone(), request.consensus_pubkey) - .await - .map(|proxy_delegation| Json(proxy_delegation).into_response()), + SigningManager::Dirk(dirk_manager) => match request.scheme { + EncryptionScheme::Bls => { + let mut manager = dirk_manager.write().await; + manager.generate_proxy_key(module_id.clone(), request.consensus_pubkey) + .await + .map(|proxy_delegation| Json(proxy_delegation).into_response()) + }, EncryptionScheme::Ecdsa => { error!("ECDSA proxy generation not supported with Dirk"); Err(SignerModuleError::DirkNotSupported) From 179dc2ed683cd9566ca3005f0b50568b96195ba8 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Fri, 14 Feb 2025 12:50:19 -0300 Subject: [PATCH 052/104] Dirk: Cleanup of dirk.rs & cargo fmt --- crates/signer/src/manager/dirk.rs | 520 ++++++++++++++++-------------- crates/signer/src/manager/mod.rs | 8 +- crates/signer/src/service.rs | 7 +- 3 files changed, 280 insertions(+), 255 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 8fdc32d3..d1531577 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -1,8 +1,14 @@ -use std::{fs, path::PathBuf}; -use std::collections::{HashMap, HashSet}; -use std::fmt::{Debug}; -use alloy::{hex, primitives::FixedBytes}; +use crate::proto::v1::DistributedAccount; +use crate::{ + error::SignerModuleError::{self, DirkCommunicationError}, + proto::v1::{ + account_manager_client::AccountManagerClient, lister_client::ListerClient, + sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, + GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, UnlockAccountRequest, + }, +}; use alloy::rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN; +use alloy::{hex, primitives::FixedBytes}; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, @@ -13,23 +19,17 @@ use cb_common::{ types::{Chain, ModuleId}, }; use rand::Rng; +use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; +use std::{fs, path::PathBuf}; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::{info, trace, warn}; use tree_hash::TreeHash; -use crate::{ - error::SignerModuleError::{self, DirkCommunicationError}, - proto::v1::{ - account_manager_client::AccountManagerClient, lister_client::ListerClient, - sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, - GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, UnlockAccountRequest, - }, -}; -use crate::proto::v1::DistributedAccount; #[derive(Clone, Debug)] enum WalletType { Simple, - Distributed + Distributed, } #[derive(Clone, Debug)] @@ -49,14 +49,12 @@ struct Account { is_proxy: bool, } - impl Account { pub fn complete_name(&self) -> String { format!("{}/{}", self.wallet, self.name) } } - #[derive(Clone, Debug)] pub struct DirkManager { chain: Chain, @@ -77,12 +75,11 @@ impl DirkManager { if let Some(ca) = &config.cert_auth { tls_config = tls_config.ca_certificate(ca.clone()); - trace!("Using custom CA certificate"); + } else { + trace!(?host.domain, "CA certificate for domain not found"); } - trace!(host.domain, "Using custom server domain"); tls_config = tls_config.domain_name(host.domain.unwrap_or_default()); - tls_configs.push(tls_config); } @@ -112,16 +109,19 @@ impl DirkManager { for host in config.hosts { let domain = host.domain.unwrap_or_default(); - let channel = channels.get(&domain).ok_or(eyre::eyre!( - "Couldn't connect to Dirk with domain {domain}" - ))?.clone(); + let channel = channels + .get(&domain) + .ok_or(eyre::eyre!("Couldn't connect to Dirk with domain {domain}"))? + .clone(); let (dirk_accounts, dirk_distributed_accounts) = get_accounts_in_wallets( channel.clone(), - host.accounts.iter() + host.accounts + .iter() .filter_map(|account| Some(account.split_once("/")?.0.to_string())) .collect(), - ).await?; + ) + .await?; for account_name in host.accounts.clone() { let (wallet, _) = account_name.split_once("/").ok_or(eyre::eyre!( @@ -129,62 +129,67 @@ impl DirkManager { ))?; // Handle simple accounts - for dirk_account in dirk_accounts.iter() - .filter(|a| { - a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) - }) - { + for dirk_account in dirk_accounts.iter().filter(|a| { + a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) + }) { let public_key = BlsPublicKey::try_from(dirk_account.public_key.as_slice())?; - let key_name = dirk_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); - trace!(?dirk_account.name, ?key_name, "Adding account to hashmap"); + let key_name = + dirk_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + trace!(?dirk_account.name, "Adding account to hashmap"); let is_proxy = key_name.contains('/'); - accounts.insert(hex::encode(public_key), Account { - wallet: wallet.to_string(), - name: key_name.to_string(), - public_key: Some(public_key), - hosts: vec![HostInfo { domain: domain.clone(), participant_id: 1 }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy, - }); + accounts.insert( + hex::encode(public_key), + Account { + wallet: wallet.to_string(), + name: key_name.to_string(), + public_key: Some(public_key), + hosts: vec![HostInfo { domain: domain.clone(), participant_id: 1 }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy, + }, + ); } // Handle distributed accounts - for dist_account in dirk_distributed_accounts.iter() - .filter(|a| { - a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) - }) - { - let public_key = BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; - let key_name = dist_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + for dist_account in dirk_distributed_accounts.iter().filter(|a| { + a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) + }) { + let public_key = + BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; + let key_name = + dist_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); let is_proxy = key_name.contains('/'); - trace!(?dist_account.name, ?key_name, "Adding distributed account to hashmap"); - + trace!(?dist_account.name, "Adding distributed account to hashmap"); + // Find the participant ID for this host from the participants list - let participant_id = dist_account.participants.iter() + let participant_id = dist_account + .participants + .iter() .find(|p| p.name == domain) .map(|p| p.id) - .ok_or_else(|| eyre::eyre!("Host {} not found in distributed account participants", domain))?; - + .ok_or_else(|| { + eyre::eyre!( + "Host {} not found in distributed account participants", + domain + ) + })?; + accounts .entry(hex::encode(public_key)) .and_modify(|account| { if !account.hosts.iter().any(|host| host.domain == domain) { - account.hosts.push(HostInfo { - domain: domain.clone(), - participant_id, - }); + account + .hosts + .push(HostInfo { domain: domain.clone(), participant_id }); } }) .or_insert_with(|| Account { wallet: wallet.to_string(), name: key_name.to_string(), public_key: Some(public_key), - hosts: vec![HostInfo { - domain: domain.clone(), - participant_id, - }], + hosts: vec![HostInfo { domain: domain.clone(), participant_id }], wallet_type: WalletType::Distributed, signing_threshold: dist_account.signing_threshold, is_proxy, @@ -193,8 +198,6 @@ impl DirkManager { } } - trace!(?accounts, "Accounts by host"); - Ok(Self { chain, channels, @@ -219,10 +222,9 @@ impl DirkManager { /// Get all available accounts in the `self.accounts` wallets pub async fn get_all_accounts(&self) -> Result, SignerModuleError> { - trace!("CALLED"); let mut seen_accounts: HashSet> = HashSet::new(); let mut unique_accounts = Vec::new(); - + // Query all channels and combine results for channel in self.channels.values() { let (accounts, distributed_accounts) = get_accounts_in_wallets( @@ -230,7 +232,7 @@ impl DirkManager { self.accounts_non_proxy().iter().map(|account| account.wallet.clone()).collect(), ) .await?; - + // There might be duplicates since we're querying several hosts, // so only keep unique ones for account in accounts { @@ -238,7 +240,7 @@ impl DirkManager { unique_accounts.push(account); } } - // TODO perhaps this is not what we want + for account in distributed_accounts { if seen_accounts.insert(account.composite_public_key.clone()) { unique_accounts.push(DirkAccount { @@ -249,7 +251,7 @@ impl DirkManager { } } } - + Ok(unique_accounts) } @@ -402,6 +404,7 @@ impl DirkManager { ) .to_string_lossy() .to_string(); + trace!(%account_dir, "Storing password in file"); fs::create_dir_all(account_dir.clone()).map_err(|err| { SignerModuleError::Internal(format!("error creating dir '{account_dir}': {err}")) @@ -417,21 +420,19 @@ impl DirkManager { fn get_channel_for_pubkey(&self, pubkey: &BlsPublicKey) -> Result { trace!(%pubkey, "Getting channel for public key"); let key = hex::encode(pubkey); - let account = self.accounts.get(&key).ok_or_else(|| - SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()) - )?; - + let account = self + .accounts + .get(&key) + .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; + // Try to find any available host's channel for host in &account.hosts { if let Some(channel) = self.channels.get(&host.domain) { return Ok(channel.clone()); } } - - // If no channel is found, return an error - Err(SignerModuleError::Internal( - "No available channel found for any host".to_string() - )) + + Err(SignerModuleError::Internal("No available channel found for any host".to_string())) } /// Unlock an account. For distributed accounts this is done for all it's hosts. @@ -440,26 +441,32 @@ impl DirkManager { account: String, password: String, ) -> Result<(), SignerModuleError> { - let account_entry = self.accounts.values() - .find(|a| a.complete_name() == account) - .ok_or_else(|| SignerModuleError::Internal(format!("Account not found: {}", account)))?; - + let account_entry = + self.accounts.values().find(|a| a.complete_name() == account).ok_or_else(|| { + SignerModuleError::Internal(format!("Account not found: {}", account)) + })?; + match account_entry.wallet_type { WalletType::Distributed => { // For distributed accounts, unlock on all hosts for host in &account_entry.hosts { if let Some(channel) = self.channels.get(&host.domain) { - self.unlock_account_on_channel(channel, &account, &password, Some(&host.domain)).await?; + self.unlock_account_on_channel( + channel, + &account, + &password, + Some(&host.domain), + ) + .await?; } } } WalletType::Simple => { // For simple accounts, unlock on a single host - let channel = self.get_channel_for_pubkey( - account_entry.public_key.as_ref().ok_or_else(|| - SignerModuleError::Internal("Account has no public key".to_string()) - )? - )?; + let channel = + self.get_channel_for_pubkey(account_entry.public_key.as_ref().ok_or_else( + || SignerModuleError::Internal("Account has no public key".to_string()), + )?)?; self.unlock_account_on_channel(&channel, &account, &password, None).await?; } } @@ -483,17 +490,17 @@ impl DirkManager { }); let unlock_response = client.unlock(unlock_request).await.map_err(|err| { - DirkCommunicationError( - format!("unlock_account_on_channel error unlocking account: {err}") - ) + DirkCommunicationError(format!( + "unlock_account_on_channel error unlocking account: {err}" + )) })?; if unlock_response.get_ref().state() != ResponseState::Succeeded { let err = unlock_response.get_ref(); warn!(?err, "unlock_account_on_channel error response"); - return Err( - DirkCommunicationError("unlock_account_on_channel error response received".to_string()) - ); + return Err(DirkCommunicationError( + "unlock_account_on_channel error response received".to_string(), + )); } Ok(()) @@ -509,7 +516,8 @@ impl DirkManager { .await? .ok_or(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; - let consensus_account_info = self.accounts() + let consensus_account_info = self + .accounts() .into_iter() .find(|account| account.complete_name() == consensus_account) .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; @@ -520,74 +528,143 @@ impl DirkManager { match consensus_account_info.wallet_type { WalletType::Simple => { + trace!(account = account_name, "Sending AccountManager/Generate request to Dirk"); let channel = self.get_channel_for_pubkey(&consensus_pubkey)?; - self.generate_simple_proxy(channel, account_name, new_password, consensus_pubkey, module_id).await - } - WalletType::Distributed => { - // Pick the first available host to generate the key - let host = consensus_account_info.hosts.first().ok_or_else(|| - SignerModuleError::Internal("No hosts available for consensus account".to_string()) - )?; - - let channel = self.channels.get(&host.domain).cloned().ok_or_else(|| - SignerModuleError::Internal(format!("No channel found for host {}", host.domain)) - )?; - - let mut client = AccountManagerClient::new(channel.clone()); - let generate_request = tonic::Request::new(GenerateRequest { - account: account_name.clone(), - passphrase: new_password.as_bytes().to_vec(), - participants: consensus_account_info.hosts.len() as u32, - signing_threshold: consensus_account_info.signing_threshold, - }); + let proxy_key = make_generate_proxy_request( + GenerateRequest { + account: account_name.clone(), + passphrase: new_password.as_bytes().to_vec(), + participants: 1, + signing_threshold: 1, + }, + channel.clone(), + ) + .await?; - trace!(host = host.domain, "Sending generate request for distributed proxy key"); - let response = client.generate(generate_request).await - .map_err(|e| DirkCommunicationError(format!("Failed to generate proxy key: {e}")))?; + // Store the password for future use + self.store_password(account_name.clone(), new_password.clone())?; - if response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError("generate request returned error".to_string())); - } + // Get the consensus account info to copy host information + let consensus_account = + self.accounts.get(&hex::encode(&consensus_pubkey)).ok_or_else(|| { + SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()) + })?; - self.store_password(account_name.clone(), new_password.clone())?; + let first_host = consensus_account.hosts.first().ok_or_else(|| { + SignerModuleError::Internal( + "Consensus account has no associated hosts".to_string(), + ) + })?; + + // Remove the wallet part from the name + let account_name_without_wallet = + String::from(account_name.split_once("/").map(|(_, n)| n).unwrap_or_default()); + + let delegation = self + .insert_proxy_account( + proxy_key, + consensus_pubkey, + module_id, + Account { + wallet: consensus_account.wallet.clone(), + name: account_name_without_wallet.clone(), + public_key: Some(proxy_key), + hosts: vec![HostInfo { + domain: first_host.domain.clone(), + participant_id: first_host.participant_id, + }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy: true, + }, + ) + .await?; - trace!(?response, "Generated new proxy key"); - let proxy_key = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) - .map_err(|_| DirkCommunicationError("Invalid proxy public key".to_string()))?; + // Unlock the account for immediate use + self.unlock_account(account_name, new_password).await?; - // Add the new proxy account to our accounts map - let consensus_name = consensus_account_info.name; - let hashmap_key = hex::encode(&proxy_key); - self.accounts.insert(hashmap_key.clone(), Account { - wallet: consensus_account_info.wallet.clone(), - name: format!("{consensus_name}/{module_id}/{uuid}"), - public_key: Some(proxy_key), - hosts: consensus_account_info.hosts.clone(), - wallet_type: WalletType::Distributed, - signing_threshold: consensus_account_info.signing_threshold, - is_proxy: true, - }); - let added = self.accounts.get(&hashmap_key).ok_or(SignerModuleError::Internal( - "Failed to add new proxy account to accounts map".to_string() - ))?; - trace!(?hashmap_key, ?added, "Proxy account added"); + Ok(delegation) + } + WalletType::Distributed => { + // Pick the first available host to generate the key, Dirk will handle the peers. + let host = consensus_account_info.hosts.first().ok_or_else(|| { + SignerModuleError::Internal( + "No hosts available for consensus account".to_string(), + ) + })?; + let channel = self.channels.get(&host.domain).cloned().ok_or_else(|| { + SignerModuleError::Internal(format!( + "No channel found for host {}", + host.domain + )) + })?; - // Get delegation signature from the consensus account - let message = ProxyDelegation { delegator: consensus_pubkey, proxy: proxy_key }; - let signature = self.request_signature(consensus_pubkey, message.tree_hash_root().0).await?; - let delegation = SignedProxyDelegation { message, signature }; + trace!(host = host.domain, "Sending generate request for distributed proxy key"); + let proxy_key = make_generate_proxy_request( + GenerateRequest { + account: account_name.clone(), + passphrase: new_password.as_bytes().to_vec(), + participants: consensus_account_info.hosts.len() as u32, + signing_threshold: consensus_account_info.signing_threshold, + }, + channel.clone(), + ) + .await?; + // Store the password for future use + self.store_password(account_name.clone(), new_password.clone())?; - if let Some(store) = &self.proxy_store { - store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { - SignerModuleError::Internal(format!("error storing delegation signature: {err}")) - })?; - } + let consensus_name = consensus_account_info.name; + let delegation = self + .insert_proxy_account( + proxy_key, + consensus_pubkey, + module_id.clone(), + Account { + wallet: consensus_account_info.wallet.clone(), + name: format!("{consensus_name}/{module_id}/{uuid}"), + public_key: Some(proxy_key), + hosts: consensus_account_info.hosts.clone(), + wallet_type: WalletType::Distributed, + signing_threshold: consensus_account_info.signing_threshold, + is_proxy: true, + }, + ) + .await?; Ok(delegation) } } } + async fn insert_proxy_account( + &mut self, + proxy_key: BlsPublicKey, + delegator: BlsPublicKey, + module_id: ModuleId, + account: Account, + ) -> Result, SignerModuleError> { + let hashmap_key = hex::encode(&proxy_key); + self.accounts.insert(hashmap_key.clone(), account); + let added = self.accounts.get(&hashmap_key).ok_or(SignerModuleError::Internal( + "Failed to add new proxy account to accounts map".to_string(), + ))?; + trace!(?hashmap_key, ?added, "Proxy account added"); + + // Get delegation signature from the consensus account + let message = ProxyDelegation { delegator, proxy: proxy_key }; + let signature = self.request_signature(delegator, message.tree_hash_root().0).await?; + let delegation: SignedProxyDelegation = + SignedProxyDelegation { message, signature }; + + if let Some(store) = &self.proxy_store { + store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { + SignerModuleError::Internal(format!("error storing delegation signature: {err}")) + })?; + } + + Ok(delegation) + } + pub async fn request_signature( &self, pubkey: BlsPublicKey, @@ -595,13 +672,14 @@ impl DirkManager { ) -> Result { let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); let key = hex::encode(pubkey); - let account = self.accounts.get(&key).ok_or_else(|| - SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()) - )?; + let account = self + .accounts + .get(&key) + .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; match account.wallet_type { WalletType::Simple => { - // Existing simple account logic + // No need for extra logic since we're just signing to a single channel let channel = self.get_channel_for_pubkey(&pubkey)?; self.sign_with_channel(channel, pubkey, account, domain, object_root).await } @@ -624,12 +702,18 @@ impl DirkManager { break; } - let channel = self.channels.get(&host.domain).cloned().ok_or_else(|| - SignerModuleError::Internal(format!("No channel found for host {}", host.domain)) - )?; + let channel = self.channels.get(&host.domain).cloned().ok_or_else(|| { + SignerModuleError::Internal(format!( + "No channel found for host {}", + host.domain + )) + })?; trace!(host = host.domain, "Requesting signature shard for creating proxy"); - match self.sign_with_channel(channel, pubkey, account, domain, object_root).await { + match self + .sign_with_channel(channel, pubkey, account, domain, object_root) + .await + { Ok(signature) => { signatures.push(signature); identifiers.push(host.participant_id); @@ -638,9 +722,9 @@ impl DirkManager { participant_id = host.participant_id, "Got signature shard" ); - }, + } Err(e) => { - trace!("Failed to get signature from host {}: {}", host.domain, e); + warn!("Failed to get signature from host {}: {}", host.domain, e); continue; } } @@ -660,9 +744,11 @@ impl DirkManager { "Recovering master signature from shards" ); - aggregate_partial_signatures(&signatures, &identifiers) - .ok_or_else(|| SignerModuleError::Internal( - "Failed to recover master signature from shards".to_string())) + aggregate_partial_signatures(&signatures, &identifiers).ok_or_else(|| { + SignerModuleError::Internal( + "Failed to recover master signature from shards".to_string(), + ) + }) } } } @@ -676,7 +762,6 @@ impl DirkManager { domain: [u8; 32], object_root: [u8; 32], ) -> Result { - trace!(?account, "sign_with_channel"); let id = match account.wallet_type { WalletType::Simple => SignerId::PublicKey(pubkey.to_vec()), WalletType::Distributed => SignerId::Account(account.complete_name()), @@ -708,7 +793,6 @@ impl DirkManager { info!("Failed to sign message, account {pubkey:#} may be locked. Unlocking and retrying."); let account_name = account.complete_name(); - trace!(account = account_name, "Unlocking account"); self.unlock_account( account_name.clone(), self.read_password(account_name.clone())?, @@ -737,90 +821,6 @@ impl DirkManager { )?, )) } - - async fn generate_simple_proxy( - &mut self, - channel: Channel, - account_name: String, - new_password: String, - consensus_pubkey: BlsPublicKey, - module_id: ModuleId, - ) -> Result, SignerModuleError> { - // Generate the new proxy key - let mut client = AccountManagerClient::new(channel.clone()); - let generate_request = tonic::Request::new(GenerateRequest { - account: account_name.clone(), - passphrase: new_password.as_bytes().to_vec(), - participants: 1, - signing_threshold: 1, - }); - - trace!(account = account_name, "Sending AccountManager/Generate request to Dirk"); - let response = client - .generate(generate_request) - .await - .map_err(|err| DirkCommunicationError(format!("error on generate request: {err}")))?; - - if response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError("generate request returned error".to_string())); - } - - // Store the password for future use - self.store_password(account_name.clone(), new_password.clone())?; - - // Get the proxy public key from the response - let proxy_key = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) - .map_err(|_| DirkCommunicationError("return value is not a valid public key".to_string()))?; - - // Get the consensus account info to copy host information - let consensus_account = self.accounts.get(&hex::encode(&consensus_pubkey)) - .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; - - let first_host = consensus_account.hosts.first() - .ok_or_else(|| SignerModuleError::Internal("Consensus account has no associated hosts".to_string()))?; - - // Store the new proxy account in our accounts map - let hashmap_key = hex::encode(&proxy_key); - // Remove the wallet part from the name - let account_name_without_wallet = String::from(account_name.split_once("/") - .map(|(_, n)| n).unwrap_or_default()); - self.accounts.insert(hashmap_key.clone(), Account { - wallet: consensus_account.wallet.clone(), - name: account_name_without_wallet.clone(), - public_key: Some(proxy_key), - hosts: vec![HostInfo { - domain: first_host.domain.clone(), - participant_id: first_host.participant_id, - }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy: true, - }); - let added = self.accounts.get(&hashmap_key).ok_or(SignerModuleError::Internal( - "Failed to add new proxy account to accounts map".to_string() - ))?; - trace!(?hashmap_key, ?added, "Account added"); - - // Unlock the account for immediate use - self.unlock_account(account_name, new_password).await?; - - // Get signature for the delegation from the consensus account - let message = ProxyDelegation { - delegator: consensus_pubkey, - proxy: proxy_key, - }; - let signature = self.request_signature(consensus_pubkey, message.tree_hash_root().0).await?; - let delegation = SignedProxyDelegation { message, signature }; - - // Store the delegation if we have a proxy store configured - if let Some(store) = &self.proxy_store { - store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { - SignerModuleError::Internal(format!("error storing delegation signature: {err}")) - })?; - } - - Ok(delegation) - } } /// Get the accounts for the wallets passed as argument @@ -845,14 +845,14 @@ async fn get_accounts_in_wallets( Ok((inner.accounts, inner.distributed_accounts)) } - pub fn aggregate_partial_signatures( partials: &[BlsSignature], identifiers: &[u64], ) -> Option { + trace!("Aggregating partial signatures"); // Ensure the number of partial signatures matches the number of identifiers if partials.len() != identifiers.len() { - trace!("aggregate_partial_signatures: Invalid number of partial signatures"); + warn!("aggregate_partial_signatures: Invalid number of partial signatures"); return None; } @@ -860,7 +860,7 @@ pub fn aggregate_partial_signatures( let mut points = Vec::new(); for sig in partials { if sig.len() != BLS_SIGNATURE_BYTES_LEN { - trace!("aggregate_partial_signatures: Invalid signature length"); + warn!("aggregate_partial_signatures: Invalid signature length"); return None; } let arr: [u8; BLS_SIGNATURE_BYTES_LEN] = (*sig).into(); @@ -869,7 +869,7 @@ pub fn aggregate_partial_signatures( if let Some(point) = opt { points.push(point); } else { - trace!("aggregate_partial_signatures: Failed to deserialize signature"); + warn!("aggregate_partial_signatures: Failed to deserialize signature"); return None; } } @@ -902,3 +902,23 @@ pub fn aggregate_partial_signatures( let bytes = recovered.to_compressed(); Some(bytes.into()) } + +async fn make_generate_proxy_request( + request: GenerateRequest, + channel: Channel, +) -> Result { + let mut client = AccountManagerClient::new(channel.clone()); + let response = client + .generate(request) + .await + .map_err(|err| DirkCommunicationError(format!("error on generate request: {err}")))?; + if response.get_ref().state() != ResponseState::Succeeded { + return Err(DirkCommunicationError("generate request returned error".to_string())); + } + trace!(?response, "Generated new proxy key"); + let proxy_key = + BlsPublicKey::try_from(response.into_inner().public_key.as_slice()).map_err(|_| { + DirkCommunicationError("return value is not a valid public key".to_string()) + })?; + Ok(proxy_key) +} diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index dabb25b1..c63b7d13 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -23,7 +23,9 @@ impl SigningManager { SigningManager::Local(local_manager) => { Ok(local_manager.read().await.consensus_pubkeys().len()) } - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.read().await.consensus_pubkeys().await?.len()), + SigningManager::Dirk(dirk_manager) => { + Ok(dirk_manager.read().await.consensus_pubkeys().await?.len()) + } } } @@ -35,7 +37,9 @@ impl SigningManager { let proxies = manager.proxies(); Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) } - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.read().await.proxies().await?.len()), + SigningManager::Dirk(dirk_manager) => { + Ok(dirk_manager.read().await.proxies().await?.len()) + } } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index e325891c..043d53a3 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -214,7 +214,7 @@ async fn handle_request_signature( .request_signature(pubkey, object_root) .await .map(|sig| Json(sig).into_response()) - }, + } SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { let manager = dirk_manager.write().await; manager @@ -268,10 +268,11 @@ async fn handle_generate_proxy( SigningManager::Dirk(dirk_manager) => match request.scheme { EncryptionScheme::Bls => { let mut manager = dirk_manager.write().await; - manager.generate_proxy_key(module_id.clone(), request.consensus_pubkey) + manager + .generate_proxy_key(module_id.clone(), request.consensus_pubkey) .await .map(|proxy_delegation| Json(proxy_delegation).into_response()) - }, + } EncryptionScheme::Ecdsa => { error!("ECDSA proxy generation not supported with Dirk"); Err(SignerModuleError::DirkNotSupported) From 563298339afd2ed467a94e38709e8dd63fea6178 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Fri, 14 Feb 2025 17:02:14 -0300 Subject: [PATCH 053/104] Dirk: Update docs about configuring Dirk integration. --- docs/docs/get_started/configuration.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index df4e0b46..4fcde803 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -246,16 +246,24 @@ Dirk is a distributed key management system that can be used to sign transaction ```toml [signer.dirk] -url = "https://dirk.gateway.url" cert_path = "/path/to/client.crt" key_path = "/path/to/client.key" -accounts = ["wallet1/accountX", "wallet2/accountY"] secrets_path = "/path/to/secrets" - # Optional parameters ca_cert_path = "/path/to/ca.crt" server_domain = "server.example.com" unlock = false + +# Add one entry like this for each host +[[signer.dirk.hosts]] +domain = "localhost-1" +url = "https://localhost-1:8081" +accounts = ["SomeWallet/SomeAccount", "DistributedWallet/Account1"] + +[[signer.dirk.hosts]] +domain = "localhost-2" +url = "https://localhost-2:8082" +accounts = ["AnotherWallet/AnotherAccount", "DistributedWallet/Account1"] ``` - `cert_path` and `key_path` are the paths to the client certificate and key used to authenticate with Dirk. From 6c62516fc6443c7f9ee7799c93efc3119311eb15 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Fri, 14 Feb 2025 18:33:43 -0300 Subject: [PATCH 054/104] Dirk: Clippy & fmt fixes --- crates/signer/src/manager/dirk.rs | 115 ++++++++++++++---------------- crates/signer/src/service.rs | 2 +- 2 files changed, 55 insertions(+), 62 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index d1531577..db5dd5cf 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -1,14 +1,11 @@ -use crate::proto::v1::DistributedAccount; -use crate::{ - error::SignerModuleError::{self, DirkCommunicationError}, - proto::v1::{ - account_manager_client::AccountManagerClient, lister_client::ListerClient, - sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, - GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, UnlockAccountRequest, - }, +use std::{ + collections::{HashMap, HashSet}, + fmt::Debug, + fs, + path::PathBuf, }; -use alloy::rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN; -use alloy::{hex, primitives::FixedBytes}; + +use alloy::{hex, primitives::FixedBytes, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN}; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, @@ -19,13 +16,20 @@ use cb_common::{ types::{Chain, ModuleId}, }; use rand::Rng; -use std::collections::{HashMap, HashSet}; -use std::fmt::Debug; -use std::{fs, path::PathBuf}; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::{info, trace, warn}; use tree_hash::TreeHash; +use crate::{ + error::SignerModuleError::{self, DirkCommunicationError}, + proto::v1::{ + account_manager_client::AccountManagerClient, lister_client::ListerClient, + sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, + DistributedAccount, GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, + UnlockAccountRequest, + }, +}; + #[derive(Clone, Debug)] enum WalletType { Simple, @@ -138,18 +142,15 @@ impl DirkManager { trace!(?dirk_account.name, "Adding account to hashmap"); let is_proxy = key_name.contains('/'); - accounts.insert( - hex::encode(public_key), - Account { - wallet: wallet.to_string(), - name: key_name.to_string(), - public_key: Some(public_key), - hosts: vec![HostInfo { domain: domain.clone(), participant_id: 1 }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy, - }, - ); + accounts.insert(hex::encode(public_key), Account { + wallet: wallet.to_string(), + name: key_name.to_string(), + public_key: Some(public_key), + hosts: vec![HostInfo { domain: domain.clone(), participant_id: 1 }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy, + }); } // Handle distributed accounts @@ -435,7 +436,8 @@ impl DirkManager { Err(SignerModuleError::Internal("No available channel found for any host".to_string())) } - /// Unlock an account. For distributed accounts this is done for all it's hosts. + /// Unlock an account. For distributed accounts this is done for all it's + /// hosts. async fn unlock_account( &self, account: String, @@ -546,7 +548,7 @@ impl DirkManager { // Get the consensus account info to copy host information let consensus_account = - self.accounts.get(&hex::encode(&consensus_pubkey)).ok_or_else(|| { + self.accounts.get(&hex::encode(consensus_pubkey)).ok_or_else(|| { SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()) })?; @@ -561,23 +563,18 @@ impl DirkManager { String::from(account_name.split_once("/").map(|(_, n)| n).unwrap_or_default()); let delegation = self - .insert_proxy_account( - proxy_key, - consensus_pubkey, - module_id, - Account { - wallet: consensus_account.wallet.clone(), - name: account_name_without_wallet.clone(), - public_key: Some(proxy_key), - hosts: vec![HostInfo { - domain: first_host.domain.clone(), - participant_id: first_host.participant_id, - }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy: true, - }, - ) + .insert_proxy_account(proxy_key, consensus_pubkey, module_id, Account { + wallet: consensus_account.wallet.clone(), + name: account_name_without_wallet.clone(), + public_key: Some(proxy_key), + hosts: vec![HostInfo { + domain: first_host.domain.clone(), + participant_id: first_host.participant_id, + }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy: true, + }) .await?; // Unlock the account for immediate use @@ -586,7 +583,8 @@ impl DirkManager { Ok(delegation) } WalletType::Distributed => { - // Pick the first available host to generate the key, Dirk will handle the peers. + // Pick the first available host to generate the key, Dirk will handle the + // peers. let host = consensus_account_info.hosts.first().ok_or_else(|| { SignerModuleError::Internal( "No hosts available for consensus account".to_string(), @@ -615,20 +613,15 @@ impl DirkManager { let consensus_name = consensus_account_info.name; let delegation = self - .insert_proxy_account( - proxy_key, - consensus_pubkey, - module_id.clone(), - Account { - wallet: consensus_account_info.wallet.clone(), - name: format!("{consensus_name}/{module_id}/{uuid}"), - public_key: Some(proxy_key), - hosts: consensus_account_info.hosts.clone(), - wallet_type: WalletType::Distributed, - signing_threshold: consensus_account_info.signing_threshold, - is_proxy: true, - }, - ) + .insert_proxy_account(proxy_key, consensus_pubkey, module_id.clone(), Account { + wallet: consensus_account_info.wallet.clone(), + name: format!("{consensus_name}/{module_id}/{uuid}"), + public_key: Some(proxy_key), + hosts: consensus_account_info.hosts.clone(), + wallet_type: WalletType::Distributed, + signing_threshold: consensus_account_info.signing_threshold, + is_proxy: true, + }) .await?; Ok(delegation) @@ -643,7 +636,7 @@ impl DirkManager { module_id: ModuleId, account: Account, ) -> Result, SignerModuleError> { - let hashmap_key = hex::encode(&proxy_key); + let hashmap_key = hex::encode(proxy_key); self.accounts.insert(hashmap_key.clone(), account); let added = self.accounts.get(&hashmap_key).ok_or(SignerModuleError::Internal( "Failed to add new proxy account to accounts map".to_string(), @@ -886,7 +879,7 @@ pub fn aggregate_partial_signatures( // Compute the Lagrange coefficient for this identifier let mut numerator = Scalar::from(1u32); let mut denominator = Scalar::from(1u32); - for (other_id, _) in &shares { + for other_id in shares.keys() { if other_id != id { numerator *= Scalar::from(*other_id); denominator *= Scalar::from(*other_id) - Scalar::from(*id); diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 192d4d55..23de9f7e 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -345,7 +345,7 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result Date: Wed, 19 Feb 2025 16:07:43 -0300 Subject: [PATCH 055/104] Dirk: Remove unused packages in crates/signer/Cargo.toml --- Cargo.lock | 6 ------ crates/signer/Cargo.toml | 6 ------ 2 files changed, 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d4a2087..0d354ef7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1479,18 +1479,13 @@ dependencies = [ "alloy", "axum 0.8.1", "axum-extra", - "base64 0.21.7", "bimap", "blsful", - "blst", "cb-common", "cb-metrics", - "derive_more", "eyre", "headers", - "k256", "lazy_static", - "log", "prometheus", "prost", "rand", @@ -1500,7 +1495,6 @@ dependencies = [ "tonic-build", "tracing", "tree_hash 0.8.0", - "tree_hash_derive", "uuid 1.12.0", ] diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index d058a946..adb4e06f 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -27,11 +27,8 @@ tracing.workspace = true prometheus.workspace = true # crypto -blst.workspace = true blsful.workspace = true tree_hash.workspace = true -tree_hash_derive.workspace = true -k256.workspace = true # misc thiserror.workspace = true @@ -39,10 +36,7 @@ eyre.workspace = true uuid.workspace = true bimap.workspace = true lazy_static.workspace = true -derive_more.workspace = true rand.workspace = true -base64 = "0.21.7" -log = "0.4.25" [build-dependencies] tonic-build.workspace = true From f604f64361cb1b4560e83b1268a9e3ebeb8448b1 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Wed, 19 Feb 2025 16:23:04 -0300 Subject: [PATCH 056/104] Dirk: Update example config files --- config.example.toml | 24 +++++++++++++++++------- examples/configs/dirk_signer.toml | 8 ++++++-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/config.example.toml b/config.example.toml index 25f421e1..ad8c7654 100644 --- a/config.example.toml +++ b/config.example.toml @@ -154,29 +154,39 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # url = "https://remote.signer.url" # For Dirk signer: # [signer.dirk] -# Complete URL of a Dirk gateway -# url = "https://dirk.gateway.url" # Path to the client certificate to authenticate with Dirk # cert_path = "/path/to/client.crt" # Path to the client key # key_path = "/path/to/client.key" -# Accounts to use as consensus keys. -# accounts = ["wallet1/accountX", "wallet2/accountY"] # Path to the secrets directory where the accounts passwords are stored # secrets_path = "/path/to/secrets" # Path to the CA certificate that signed the Dirk server certificate # OPTIONAL # ca_cert_path = "/path/to/ca.crt" -# Domain name of the server to use in TLS verification, if different from the URL -# OPTIONAL -# server_domain = "server.example.com" # Whether to try to unlock the accounts on sign failure # OPTIONAL, DEFAULT: false # unlock = false + +# Add one entry like this for each Dirk host +#[[signer.dirk.hosts]] +# Domain name of the server to use in TLS verification, if different from the URL +# OPTIONAL +#domain = "localhost-1" +# Complete URL of a Dirk gateway +#url = "https://localhost:8881" +# Accounts to use as consensus keys +#accounts = ["Wallet1/Account1", "DistributedWallet/Account1"] + +#[[signer.dirk.hosts]] +#domain = "localhost-2" +#url = "https://localhost:8882" +#accounts = ["Wallet2/Account2", "DistributedWallet/Account1"] + # Configuration for how the Signer module should store proxy delegations. # OPTIONAL # [signer.dirk.store] # proxy_dir = "/path/to/proxies" + # For Local signer: # Configuration for how the Signer module should load validator keys. Currently two types of loaders are supported: # - File: load keys from a plain text file (unsafe, use only for testing purposes) diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml index 12e39295..0dc0d026 100644 --- a/examples/configs/dirk_signer.toml +++ b/examples/configs/dirk_signer.toml @@ -10,13 +10,17 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f docker_image = "commitboost_signer" [signer.dirk] -url = "https://gateway.dirk.url" cert_path = "/path/to/client.crt" key_path = "/path/to/client.key" -accounts = ["wallet1/accountX", "wallet2/accountY"] secrets_path = "./dirk_secrets" unlock = true +# Example of a single Dirk host +[[signer.dirk.hosts]] +domain = "gateway.dirk.url" +url = "https://gateway.dirk.url" +accounts = ["wallet1/accountX", "wallet2/accountY"] + [signer.dirk.store] proxy_dir = "/path/to/proxies" From 24f3e5c13585c6f45c71794ca76d8588f78fc0da Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Wed, 19 Feb 2025 16:46:01 -0300 Subject: [PATCH 057/104] Dirk: Move Arc Lock to SigningState struct instead of SigningManager enum --- crates/signer/src/manager/mod.rs | 12 +++----- crates/signer/src/service.rs | 49 ++++++++++++++++---------------- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index c63b7d13..959edfc1 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -13,7 +13,7 @@ pub mod local; #[derive(Clone)] pub enum SigningManager { Local(Arc>), - Dirk(Arc>), + Dirk(DirkManager), } impl SigningManager { @@ -23,9 +23,7 @@ impl SigningManager { SigningManager::Local(local_manager) => { Ok(local_manager.read().await.consensus_pubkeys().len()) } - SigningManager::Dirk(dirk_manager) => { - Ok(dirk_manager.read().await.consensus_pubkeys().await?.len()) - } + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.consensus_pubkeys().await?.len()), } } @@ -37,9 +35,7 @@ impl SigningManager { let proxies = manager.proxies(); Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) } - SigningManager::Dirk(dirk_manager) => { - Ok(dirk_manager.read().await.proxies().await?.len()) - } + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.proxies().await?.len()), } } @@ -52,7 +48,7 @@ impl SigningManager { local_manager.read().await.get_consensus_proxy_maps(module_id) } SigningManager::Dirk(dirk_manager) => { - dirk_manager.read().await.get_consensus_proxy_maps(module_id).await + dirk_manager.get_consensus_proxy_maps(module_id).await } } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 23de9f7e..92c3a964 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -44,7 +44,7 @@ pub struct SigningService; #[derive(Clone)] struct SigningState { /// Manager handling different signing methods - manager: SigningManager, + manager: Arc>, /// Map of JWTs to module ids. This also acts as registry of all modules /// running jwts: Arc>, @@ -67,7 +67,7 @@ impl SigningService { } SigningState { - manager: SigningManager::Dirk(Arc::new(RwLock::new(dirk_manager))), + manager: Arc::new(RwLock::new(SigningManager::Dirk(dirk_manager))), jwts: config.jwts.into(), } } @@ -88,14 +88,16 @@ impl SigningService { } SigningState { - manager: SigningManager::Local(Arc::new(RwLock::new(local_manager))), + manager: Arc::new(RwLock::new(SigningManager::Local(Arc::new(RwLock::new( + local_manager, + ))))), jwts: config.jwts.into(), } } }; - let loaded_consensus = state.manager.available_consensus_signers().await?; - let loaded_proxies = state.manager.available_proxy_signers().await?; + let loaded_consensus = state.manager.read().await.available_consensus_signers().await?; + let loaded_proxies = state.manager.read().await.available_proxy_signers().await?; info!(version = COMMIT_BOOST_VERSION, commit = COMMIT_BOOST_COMMIT, modules =? module_ids, port =? config.server_port, loaded_consensus, loaded_proxies, "Starting signing service"); @@ -167,6 +169,8 @@ async fn handle_get_pubkeys( let keys = state .manager + .read() + .await .get_consensus_proxy_maps(&module_id) .await .map_err(|err| SignerModuleError::Internal(err.to_string()))?; @@ -186,7 +190,8 @@ async fn handle_request_signature( debug!(event = "request_signature", ?module_id, %request, ?req_id, "New request"); - let res = match state.manager { + let manager = state.manager.read().await; + let res = match &*manager { SigningManager::Local(local_manager) => match request { SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => local_manager .read() @@ -212,16 +217,12 @@ async fn handle_request_signature( } }, SigningManager::Dirk(dirk_manager) => match request { - SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => { - let manager = dirk_manager.write().await; - manager - .request_signature(pubkey, object_root) - .await - .map(|sig| Json(sig).into_response()) - } + SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => dirk_manager + .request_signature(pubkey, object_root) + .await + .map(|sig| Json(sig).into_response()), SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { - let manager = dirk_manager.write().await; - manager + dirk_manager .request_signature(bls_key, object_root) .await .map(|sig| Json(sig).into_response()) @@ -254,7 +255,8 @@ async fn handle_generate_proxy( debug!(event = "generate_proxy", ?module_id, scheme=?request.scheme, pubkey=%request.consensus_pubkey, ?req_id, "New request"); - let res = match state.manager { + let mut manager = state.manager.write().await; + let res = match &mut *manager { SigningManager::Local(local_manager) => match request.scheme { EncryptionScheme::Bls => local_manager .write() @@ -270,13 +272,10 @@ async fn handle_generate_proxy( .map(|proxy_delegation| Json(proxy_delegation).into_response()), }, SigningManager::Dirk(dirk_manager) => match request.scheme { - EncryptionScheme::Bls => { - let mut manager = dirk_manager.write().await; - manager - .generate_proxy_key(module_id.clone(), request.consensus_pubkey) - .await - .map(|proxy_delegation| Json(proxy_delegation).into_response()) - } + EncryptionScheme::Bls => dirk_manager + .generate_proxy_key(module_id.clone(), request.consensus_pubkey) + .await + .map(|proxy_delegation| Json(proxy_delegation).into_response()), EncryptionScheme::Ecdsa => { error!("ECDSA proxy generation not supported with Dirk"); Err(SignerModuleError::DirkNotSupported) @@ -314,7 +313,7 @@ async fn handle_reload( } }; - state.manager = new_manager; + state.manager = Arc::new(RwLock::new(new_manager)); Ok((StatusCode::OK, "OK")) } @@ -334,7 +333,7 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result { let mut manager = LocalSigningManager::new(config.chain, proxy_store)?; From 408ff132793f006d52d1a4b0cdb4dae8e5caadcf Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Wed, 19 Feb 2025 16:47:46 -0300 Subject: [PATCH 058/104] Dirk: Pre-allocate space in tls_configs vector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel Iñaki Bilbao --- crates/signer/src/manager/dirk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index db5dd5cf..5f05d80c 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -71,7 +71,7 @@ pub struct DirkManager { impl DirkManager { pub async fn new_from_config(chain: Chain, config: DirkConfig) -> eyre::Result { - let mut tls_configs = Vec::new(); + let mut tls_configs = Vec::with_capacity(config.hosts.len()); // Create a TLS config for each host for host in config.hosts.clone() { From bb0c37b438008ef08c00b34954264a4d617075ed Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Wed, 19 Feb 2025 16:49:28 -0300 Subject: [PATCH 059/104] Dirk: Pre-allocate space in signatures and identifiers vectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel Iñaki Bilbao --- crates/signer/src/manager/dirk.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 5f05d80c..9a4f3408 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -677,8 +677,8 @@ impl DirkManager { self.sign_with_channel(channel, pubkey, account, domain, object_root).await } WalletType::Distributed => { - let mut signatures = Vec::new(); - let mut identifiers = Vec::new(); + let mut signatures = Vec::with_capacity(account.hosts.len()); + let mut identifiers = Vec::with_capacity(account.hosts.len()); let num_hosts_needed = account.signing_threshold as usize; if account.hosts.len() < num_hosts_needed { From 27eb6831420584e2739bfc83e33dfb8ada4aefc7 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Wed, 19 Feb 2025 17:04:58 -0300 Subject: [PATCH 060/104] Dirk: Rename host.domain -> host.server_name --- config.example.toml | 4 +- crates/common/src/config/signer.rs | 2 +- crates/signer/src/manager/dirk.rs | 56 +++++++++++++------------- docs/docs/get_started/configuration.md | 4 +- examples/configs/dirk_signer.toml | 2 +- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/config.example.toml b/config.example.toml index ad8c7654..1f61f0dc 100644 --- a/config.example.toml +++ b/config.example.toml @@ -171,14 +171,14 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f #[[signer.dirk.hosts]] # Domain name of the server to use in TLS verification, if different from the URL # OPTIONAL -#domain = "localhost-1" +#server_name = "localhost-1" # Complete URL of a Dirk gateway #url = "https://localhost:8881" # Accounts to use as consensus keys #accounts = ["Wallet1/Account1", "DistributedWallet/Account1"] #[[signer.dirk.hosts]] -#domain = "localhost-2" +#server_name = "localhost-2" #url = "https://localhost:8882" #accounts = ["Wallet2/Account2", "DistributedWallet/Account1"] diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index b09ccd52..cbb7dda4 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -36,7 +36,7 @@ fn default_signer() -> String { #[serde(rename_all = "snake_case")] pub struct DirkHost { /// Domain name of the server to use in TLS verification - pub domain: Option, + pub server_name: Option, /// Complete URL of a Dirk gateway pub url: Url, /// Accounts used as consensus keys diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index db5dd5cf..ccb51f3b 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -38,7 +38,7 @@ enum WalletType { #[derive(Clone, Debug)] struct HostInfo { - domain: String, + server_name: String, participant_id: u64, } @@ -62,7 +62,7 @@ impl Account { #[derive(Clone, Debug)] pub struct DirkManager { chain: Chain, - channels: HashMap, // domain -> channel + channels: HashMap, // server_name -> channel accounts: HashMap, // pubkey -> account unlock: bool, secrets_path: PathBuf, @@ -80,10 +80,10 @@ impl DirkManager { if let Some(ca) = &config.cert_auth { tls_config = tls_config.ca_certificate(ca.clone()); } else { - trace!(?host.domain, "CA certificate for domain not found"); + trace!(?host.server_name, "CA certificate for server name not found"); } - tls_config = tls_config.domain_name(host.domain.unwrap_or_default()); + tls_config = tls_config.domain_name(host.server_name.unwrap_or_default()); tls_configs.push(tls_config); } @@ -92,7 +92,7 @@ impl DirkManager { let mut channels = HashMap::new(); for (i, tls_config) in tls_configs.iter().enumerate() { let config_host = config.hosts[i].clone(); - let domain = config_host.domain.unwrap_or_default(); + let server_name = config_host.server_name.unwrap_or_default(); match Channel::from_shared(config_host.url.to_string()) .map_err(|_| eyre::eyre!("Invalid Dirk URL"))? .tls_config(tls_config.clone()) @@ -101,10 +101,10 @@ impl DirkManager { .await { Ok(ch) => { - channels.insert(domain, ch); + channels.insert(server_name, ch); } Err(e) => { - trace!("Couldn't connect to Dirk with domain {}: {e}", domain); + trace!("Couldn't connect to Dirk with server name {}: {e}", server_name); } } } @@ -112,10 +112,10 @@ impl DirkManager { let mut accounts: HashMap = HashMap::new(); for host in config.hosts { - let domain = host.domain.unwrap_or_default(); + let server_name = host.server_name.unwrap_or_default(); let channel = channels - .get(&domain) - .ok_or(eyre::eyre!("Couldn't connect to Dirk with domain {domain}"))? + .get(&server_name) + .ok_or(eyre::eyre!("Couldn't connect to Dirk with server name {server_name}"))? .clone(); let (dirk_accounts, dirk_distributed_accounts) = get_accounts_in_wallets( @@ -146,7 +146,7 @@ impl DirkManager { wallet: wallet.to_string(), name: key_name.to_string(), public_key: Some(public_key), - hosts: vec![HostInfo { domain: domain.clone(), participant_id: 1 }], + hosts: vec![HostInfo { server_name: server_name.clone(), participant_id: 1 }], wallet_type: WalletType::Simple, signing_threshold: 1, is_proxy, @@ -168,29 +168,29 @@ impl DirkManager { let participant_id = dist_account .participants .iter() - .find(|p| p.name == domain) + .find(|p| p.name == server_name) .map(|p| p.id) .ok_or_else(|| { eyre::eyre!( "Host {} not found in distributed account participants", - domain + server_name ) })?; accounts .entry(hex::encode(public_key)) .and_modify(|account| { - if !account.hosts.iter().any(|host| host.domain == domain) { + if !account.hosts.iter().any(|host| host.server_name == server_name) { account .hosts - .push(HostInfo { domain: domain.clone(), participant_id }); + .push(HostInfo { server_name: server_name.clone(), participant_id }); } }) .or_insert_with(|| Account { wallet: wallet.to_string(), name: key_name.to_string(), public_key: Some(public_key), - hosts: vec![HostInfo { domain: domain.clone(), participant_id }], + hosts: vec![HostInfo { server_name: server_name.clone(), participant_id }], wallet_type: WalletType::Distributed, signing_threshold: dist_account.signing_threshold, is_proxy, @@ -428,7 +428,7 @@ impl DirkManager { // Try to find any available host's channel for host in &account.hosts { - if let Some(channel) = self.channels.get(&host.domain) { + if let Some(channel) = self.channels.get(&host.server_name) { return Ok(channel.clone()); } } @@ -452,12 +452,12 @@ impl DirkManager { WalletType::Distributed => { // For distributed accounts, unlock on all hosts for host in &account_entry.hosts { - if let Some(channel) = self.channels.get(&host.domain) { + if let Some(channel) = self.channels.get(&host.server_name) { self.unlock_account_on_channel( channel, &account, &password, - Some(&host.domain), + Some(&host.server_name), ) .await?; } @@ -568,7 +568,7 @@ impl DirkManager { name: account_name_without_wallet.clone(), public_key: Some(proxy_key), hosts: vec![HostInfo { - domain: first_host.domain.clone(), + server_name: first_host.server_name.clone(), participant_id: first_host.participant_id, }], wallet_type: WalletType::Simple, @@ -590,14 +590,14 @@ impl DirkManager { "No hosts available for consensus account".to_string(), ) })?; - let channel = self.channels.get(&host.domain).cloned().ok_or_else(|| { + let channel = self.channels.get(&host.server_name).cloned().ok_or_else(|| { SignerModuleError::Internal(format!( "No channel found for host {}", - host.domain + host.server_name )) })?; - trace!(host = host.domain, "Sending generate request for distributed proxy key"); + trace!(host = host.server_name, "Sending generate request for distributed proxy key"); let proxy_key = make_generate_proxy_request( GenerateRequest { account: account_name.clone(), @@ -695,14 +695,14 @@ impl DirkManager { break; } - let channel = self.channels.get(&host.domain).cloned().ok_or_else(|| { + let channel = self.channels.get(&host.server_name).cloned().ok_or_else(|| { SignerModuleError::Internal(format!( "No channel found for host {}", - host.domain + host.server_name )) })?; - trace!(host = host.domain, "Requesting signature shard for creating proxy"); + trace!(host = host.server_name, "Requesting signature shard for creating proxy"); match self .sign_with_channel(channel, pubkey, account, domain, object_root) .await @@ -711,13 +711,13 @@ impl DirkManager { signatures.push(signature); identifiers.push(host.participant_id); trace!( - host = host.domain, + host = host.server_name, participant_id = host.participant_id, "Got signature shard" ); } Err(e) => { - warn!("Failed to get signature from host {}: {}", host.domain, e); + warn!("Failed to get signature from host {}: {}", host.server_name, e); continue; } } diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 4fcde803..c08fbe65 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -256,12 +256,12 @@ unlock = false # Add one entry like this for each host [[signer.dirk.hosts]] -domain = "localhost-1" +server_name = "localhost-1" url = "https://localhost-1:8081" accounts = ["SomeWallet/SomeAccount", "DistributedWallet/Account1"] [[signer.dirk.hosts]] -domain = "localhost-2" +server_name = "localhost-2" url = "https://localhost-2:8082" accounts = ["AnotherWallet/AnotherAccount", "DistributedWallet/Account1"] ``` diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml index 0dc0d026..e476b07b 100644 --- a/examples/configs/dirk_signer.toml +++ b/examples/configs/dirk_signer.toml @@ -17,7 +17,7 @@ unlock = true # Example of a single Dirk host [[signer.dirk.hosts]] -domain = "gateway.dirk.url" +server_name = "gateway.dirk.url" url = "https://gateway.dirk.url" accounts = ["wallet1/accountX", "wallet2/accountY"] From 46033ea281cf1d9570c1c8a6dc701e95fff0f2e7 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Wed, 19 Feb 2025 17:12:38 -0300 Subject: [PATCH 061/104] Dirk: Do not call tls_config.domain_name() when server_name is not present --- crates/signer/src/manager/dirk.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index ccb51f3b..81dc819e 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -83,8 +83,12 @@ impl DirkManager { trace!(?host.server_name, "CA certificate for server name not found"); } - tls_config = tls_config.domain_name(host.server_name.unwrap_or_default()); - tls_configs.push(tls_config); + if let Some(server_name) = host.server_name.clone() { + tls_config = tls_config.domain_name(server_name); + tls_configs.push(tls_config); + } else { + trace!("Server name for host {0} not present", host.url); + } } // Create a channel for each host, attempt to connect, From 51fe16dcb1b307f9a0b6450ad3ad3ee62a7cfcda Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Wed, 19 Feb 2025 17:47:12 -0300 Subject: [PATCH 062/104] Dirk: Check if host is proxy with pattern // --- crates/signer/src/manager/dirk.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 81dc819e..70ed55a3 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -141,10 +141,10 @@ impl DirkManager { a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) }) { let public_key = BlsPublicKey::try_from(dirk_account.public_key.as_slice())?; - let key_name = - dirk_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + let key_name = dirk_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); trace!(?dirk_account.name, "Adding account to hashmap"); - let is_proxy = key_name.contains('/'); + + let is_proxy = is_proxy_key_name(key_name); accounts.insert(hex::encode(public_key), Account { wallet: wallet.to_string(), @@ -161,11 +161,11 @@ impl DirkManager { for dist_account in dirk_distributed_accounts.iter().filter(|a| { a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) }) { - let public_key = - BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; - let key_name = - dist_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); - let is_proxy = key_name.contains('/'); + let public_key = BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; + let key_name = dist_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + + let is_proxy = is_proxy_key_name(key_name); + trace!(?dist_account.name, "Adding distributed account to hashmap"); // Find the participant ID for this host from the participants list @@ -919,3 +919,11 @@ async fn make_generate_proxy_request( })?; Ok(proxy_key) } + +/// Checks if a key name follows the proxy pattern // +fn is_proxy_key_name(key_name: &str) -> bool { + key_name.split('/').count() == 3 && { + let parts: Vec<&str> = key_name.split('/').collect(); + uuid::Uuid::parse_str(parts[2]).is_ok() // Verify the last part is a valid UUID + } +} From b81d5ff32343655520965a6df6aa600845f3c24f Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Thu, 20 Feb 2025 09:56:34 -0300 Subject: [PATCH 063/104] Dirk: Simplify service.rs --- crates/common/src/config/signer.rs | 2 +- crates/signer/src/service.rs | 38 +++--------------------------- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index cbb7dda4..f950de4e 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -87,7 +87,7 @@ pub struct DirkConfig { pub unlock: bool, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct StartSignerConfig { pub chain: Chain, pub loader: Option, diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 92c3a964..5c10a5e8 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -59,41 +59,9 @@ impl SigningService { let module_ids: Vec = config.jwts.left_values().cloned().map(Into::into).collect(); - let state = match config.dirk { - Some(dirk) => { - let mut dirk_manager = DirkManager::new_from_config(config.chain, dirk).await?; - if let Some(store) = config.store { - dirk_manager = dirk_manager.with_proxy_store(store.init_from_env()?)?; - } - - SigningState { - manager: Arc::new(RwLock::new(SigningManager::Dirk(dirk_manager))), - jwts: config.jwts.into(), - } - } - None => { - let proxy_store = if let Some(store) = config.store { - Some(store.init_from_env()?) - } else { - warn!("Proxy store not configured. Proxies keys and delegations will not be persisted"); - None - }; - - let mut local_manager = LocalSigningManager::new(config.chain, proxy_store)?; - - if let Some(loader) = config.loader { - for signer in loader.load_keys()? { - local_manager.add_consensus_signer(signer); - } - } - - SigningState { - manager: Arc::new(RwLock::new(SigningManager::Local(Arc::new(RwLock::new( - local_manager, - ))))), - jwts: config.jwts.into(), - } - } + let state = SigningState { + manager: Arc::new(RwLock::new(start_manager(config.clone()).await?)), + jwts: config.jwts.into(), }; let loaded_consensus = state.manager.read().await.available_consensus_signers().await?; From db3fc957f899c58d1821b72f1c0834759929b594 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Thu, 20 Feb 2025 10:36:08 -0300 Subject: [PATCH 064/104] Dirk: Retry connection in unlock_account_on_channel --- crates/signer/src/manager/dirk.rs | 56 ++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 3a1e6763..810d4e4c 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -488,28 +488,46 @@ impl DirkManager { host_domain: Option<&str>, ) -> Result<(), SignerModuleError> { trace!(account, host = host_domain, "unlock_account_on_channel"); + const MAX_RETRIES: u32 = 5; + let mut retry_count = 0; + + loop { + let mut client = AccountManagerClient::new(channel.clone()); + let unlock_request = tonic::Request::new(UnlockAccountRequest { + account: account.to_string(), + passphrase: password.as_bytes().to_vec(), + }); - let mut client = AccountManagerClient::new(channel.clone()); - let unlock_request = tonic::Request::new(UnlockAccountRequest { - account: account.to_string(), - passphrase: password.as_bytes().to_vec(), - }); - - let unlock_response = client.unlock(unlock_request).await.map_err(|err| { - DirkCommunicationError(format!( - "unlock_account_on_channel error unlocking account: {err}" - )) - })?; + match client.unlock(unlock_request).await { + Ok(unlock_response) => { + if unlock_response.get_ref().state() == ResponseState::Succeeded { + return Ok(()); + } + // We have connected but an error has been returned + let err = unlock_response.get_ref(); + warn!(?err, "unlock_account_on_channel error response"); + return Err(DirkCommunicationError( + "unlock_account_on_channel error response received".to_string(), + )); + } + Err(status) => { + retry_count += 1; + if retry_count >= MAX_RETRIES { + return Err(DirkCommunicationError(format!( + "Failed to connect after {MAX_RETRIES} attempts: {status}" + ))); + } - if unlock_response.get_ref().state() != ResponseState::Succeeded { - let err = unlock_response.get_ref(); - warn!(?err, "unlock_account_on_channel error response"); - return Err(DirkCommunicationError( - "unlock_account_on_channel error response received".to_string(), - )); + warn!( + ?status, + retry_count, + host = host_domain, + "Connection failed, retrying in 3 seconds..." + ); + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + } + } } - - Ok(()) } pub async fn generate_proxy_key( From 215ed7039adb89bfeb733ad521eb40101f39842c Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Thu, 20 Feb 2025 14:59:07 -0300 Subject: [PATCH 065/104] Dirk: Run signature requests in parallel --- crates/signer/src/manager/dirk.rs | 85 ++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 810d4e4c..44df455a 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -19,6 +19,7 @@ use rand::Rng; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::{info, trace, warn}; use tree_hash::TreeHash; +use tokio::task::JoinSet; use crate::{ error::SignerModuleError::{self, DirkCommunicationError}, @@ -690,17 +691,15 @@ impl DirkManager { let account = self .accounts .get(&key) - .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; + .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))? + .clone(); match account.wallet_type { WalletType::Simple => { - // No need for extra logic since we're just signing to a single channel let channel = self.get_channel_for_pubkey(&pubkey)?; - self.sign_with_channel(channel, pubkey, account, domain, object_root).await + self.sign_with_channel(channel, pubkey, &account, domain, object_root).await } WalletType::Distributed => { - let mut signatures = Vec::with_capacity(account.hosts.len()); - let mut identifiers = Vec::with_capacity(account.hosts.len()); let num_hosts_needed = account.signing_threshold as usize; if account.hosts.len() < num_hosts_needed { @@ -711,36 +710,64 @@ impl DirkManager { ))); } - // Try to get signatures from hosts + let mut set = JoinSet::new(); + + // Spawn tasks for each host for host in &account.hosts { + let Some(channel) = self.channels.get(&host.server_name).cloned() else { + continue; + }; + let dirk = self.clone(); + let account = account.clone(); + let server_name = host.server_name.clone(); + let participant_id = host.participant_id; + + set.spawn(async move { + trace!(host = server_name, "Requesting signature shard for creating proxy"); + + match dirk + .sign_with_channel(channel, pubkey, &account, domain, object_root) + .await + { + Ok(signature) => { + trace!( + host = server_name, + participant_id = participant_id, + "Got signature shard" + ); + Ok((signature, participant_id)) + } + Err(e) => { + warn!("Failed to get signature from host {}: {}", server_name, e); + Err(e) + } + } + }); + } + + let mut signatures = Vec::new(); + let mut identifiers = Vec::new(); + + // Collect results until we have enough signatures + while let Some(result) = set.join_next().await { + // Check if we already have enough signatures before processing more if signatures.len() >= num_hosts_needed { + trace!("Already have enough signatures ({}/{}), cancelling remaining tasks", + signatures.len(), num_hosts_needed); + set.abort_all(); break; } - let channel = self.channels.get(&host.server_name).cloned().ok_or_else(|| { - SignerModuleError::Internal(format!( - "No channel found for host {}", - host.server_name - )) - })?; + if let Ok(Ok((signature, id))) = result { + signatures.push(signature); + identifiers.push(id); + trace!("Got signature {}/{}", signatures.len(), num_hosts_needed); - trace!(host = host.server_name, "Requesting signature shard for creating proxy"); - match self - .sign_with_channel(channel, pubkey, account, domain, object_root) - .await - { - Ok(signature) => { - signatures.push(signature); - identifiers.push(host.participant_id); - trace!( - host = host.server_name, - participant_id = host.participant_id, - "Got signature shard" - ); - } - Err(e) => { - warn!("Failed to get signature from host {}: {}", host.server_name, e); - continue; + if signatures.len() >= num_hosts_needed { + trace!("Already have enough signatures ({}/{}), cancelling remaining tasks", + signatures.len(), num_hosts_needed); + set.abort_all(); + break; } } } From 5d636181bcd8cac521154280e642710e493bb4a3 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Thu, 20 Feb 2025 16:18:19 -0300 Subject: [PATCH 066/104] Dirk: Remove get_all_accounts and use hashmap directly. --- crates/signer/src/manager/dirk.rs | 137 +++++++----------------------- crates/signer/src/manager/mod.rs | 4 +- 2 files changed, 34 insertions(+), 107 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 44df455a..0b273659 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -226,116 +226,43 @@ impl DirkManager { Ok(Self { proxy_store: Some(proxy_store), ..self }) } - /// Get all available accounts in the `self.accounts` wallets - pub async fn get_all_accounts(&self) -> Result, SignerModuleError> { - let mut seen_accounts: HashSet> = HashSet::new(); - let mut unique_accounts = Vec::new(); - - // Query all channels and combine results - for channel in self.channels.values() { - let (accounts, distributed_accounts) = get_accounts_in_wallets( - channel.clone(), - self.accounts_non_proxy().iter().map(|account| account.wallet.clone()).collect(), - ) - .await?; - - // There might be duplicates since we're querying several hosts, - // so only keep unique ones - for account in accounts { - if seen_accounts.insert(account.public_key.clone()) { - unique_accounts.push(account); - } - } - - for account in distributed_accounts { - if seen_accounts.insert(account.composite_public_key.clone()) { - unique_accounts.push(DirkAccount { - name: account.name, - public_key: account.composite_public_key, - uuid: account.uuid, - }); - } - } - } - - Ok(unique_accounts) - } - /// Get the complete account name (`wallet/account`) for a public key. /// Returns `Ok(None)` if the account was not found. /// Returns `Err` if there was a communication error with Dirk. - async fn get_pubkey_account( + fn get_pubkey_account( &self, pubkey: BlsPublicKey, - ) -> Result, SignerModuleError> { + ) -> Option { match self .accounts() .iter() - .find(|account| account.public_key.is_some_and(|account_pk| account_pk == pubkey)) - { - Some(account) => Ok(Some(account.complete_name())), - None => { - let accounts = self.get_all_accounts().await?; - - for account in accounts { - if account.public_key == pubkey.to_vec() { - return Ok(Some(account.name)); - } - } - - Ok(None) + .find(|account| + account.public_key.is_some_and(|account_pk| account_pk == pubkey)) { + Some(account) => Some(account.complete_name()), + None => None, } - } } /// Returns the public keys of the config-registered accounts - pub async fn consensus_pubkeys(&self) -> eyre::Result> { - let registered_pubkeys = self + pub async fn consensus_pubkeys(&self) -> Vec { + self .accounts_non_proxy() .iter() .filter_map(|account| account.public_key) - .collect::>(); - - if registered_pubkeys.len() == self.accounts.len() { - Ok(registered_pubkeys) - } else { - let accounts = self.get_all_accounts().await?; - - let expected_accounts: Vec = - self.accounts().iter().map(|account| account.complete_name()).collect(); - - Ok(accounts - .iter() - .filter_map(|account| { - if expected_accounts.contains(&account.name) { - BlsPublicKey::try_from(account.public_key.as_slice()).ok() - } else { - None - } - }) - .collect()) - } + .collect::>() } /// Returns the public keys of all the proxy accounts found in Dirk. /// An account is considered a proxy if its name has the format /// `consensus_account/module_id/uuid`, where `consensus_account` is the /// name of a config-registered account. - pub async fn proxies(&self) -> eyre::Result> { - let accounts = self.get_all_accounts().await?; - - Ok(accounts + pub async fn proxies(&self) -> Vec { + self + .accounts() .iter() - .filter_map(|account| { - if self.accounts().iter().any(|consensus_account| { - account.name.starts_with(&format!("{}/", consensus_account.complete_name())) - }) { - BlsPublicKey::try_from(account.public_key.as_slice()).ok() - } else { - None - } - }) - .collect()) + .filter(|account| account.is_proxy) + .filter_map(|account| account.public_key) + .collect::>() } /// Returns a mapping of the proxy accounts' pubkeys by consensus account, @@ -347,28 +274,29 @@ impl DirkManager { &self, module_id: &ModuleId, ) -> Result, SignerModuleError> { - let accounts = self.get_all_accounts().await?; + let consensus_accounts = self.accounts_non_proxy(); + let accounts = self.accounts(); + let proxy_accounts: Vec<_> = accounts + .iter() + .filter(|account| account.is_proxy) + .collect(); let mut proxy_maps = Vec::new(); - - for consensus_account in self.accounts_non_proxy().iter() { + for consensus_account in consensus_accounts { let Some(consensus_key) = consensus_account.public_key else { + trace!("get_consensus_proxy_maps: skipping"); continue; }; - - let proxy_keys = accounts - .iter() - .filter_map(|account| { - if account - .name - .starts_with(&format!("{}/{module_id}/", consensus_account.complete_name())) - { - BlsPublicKey::try_from(account.public_key.as_slice()).ok() - } else { - None + let mut proxy_keys: Vec = vec![]; + let start_of_proxy_name = format!("{}/{module_id}", consensus_account.complete_name()); + for proxy in &proxy_accounts { + trace!(%proxy.name, %start_of_proxy_name, "get_consensus_proxy_maps: checking if name starts with"); + if proxy.complete_name().starts_with(&start_of_proxy_name) { + if let Some(pubkey) = proxy.public_key { + proxy_keys.push(pubkey); } - }) - .collect::>(); + } + } proxy_maps.push(ConsensusProxyMap { consensus: consensus_key, proxy_bls: proxy_keys, @@ -538,7 +466,6 @@ impl DirkManager { ) -> Result, SignerModuleError> { let consensus_account = self .get_pubkey_account(consensus_pubkey) - .await? .ok_or(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; let consensus_account_info = self diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index 959edfc1..87aee2af 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -23,7 +23,7 @@ impl SigningManager { SigningManager::Local(local_manager) => { Ok(local_manager.read().await.consensus_pubkeys().len()) } - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.consensus_pubkeys().await?.len()), + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.consensus_pubkeys().await.len()), } } @@ -35,7 +35,7 @@ impl SigningManager { let proxies = manager.proxies(); Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) } - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.proxies().await?.len()), + SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.proxies().await.len()), } } From 93821c3f41e5d8962615103abf6379315e9ae7b4 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Fri, 21 Feb 2025 10:52:26 -0300 Subject: [PATCH 067/104] Dirk: Build the proxy maps only at the beginning. --- crates/signer/src/manager/dirk.rs | 247 +++++++++++++++++++----------- crates/signer/src/manager/mod.rs | 2 +- 2 files changed, 155 insertions(+), 94 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 0b273659..50821474 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -5,8 +5,19 @@ use std::{ path::PathBuf, }; +use crate::{ + error::SignerModuleError::{self, DirkCommunicationError}, + proto::v1::{ + account_manager_client::AccountManagerClient, lister_client::ListerClient, + sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, + DistributedAccount, GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, + UnlockAccountRequest, + }, +}; +use alloy::signers::k256::sha2::digest::typenum::Mod; use alloy::{hex, primitives::FixedBytes, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN}; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; +use cb_common::signer::EcdsaPublicKey; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, config::DirkConfig, @@ -16,20 +27,10 @@ use cb_common::{ types::{Chain, ModuleId}, }; use rand::Rng; +use tokio::task::JoinSet; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::{info, trace, warn}; use tree_hash::TreeHash; -use tokio::task::JoinSet; - -use crate::{ - error::SignerModuleError::{self, DirkCommunicationError}, - proto::v1::{ - account_manager_client::AccountManagerClient, lister_client::ListerClient, - sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, - DistributedAccount, GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, - UnlockAccountRequest, - }, -}; #[derive(Clone, Debug)] enum WalletType { @@ -60,6 +61,12 @@ impl Account { } } +#[derive(Clone, Debug)] +struct ModuleConsensusProxyMap { + pub consensus: BlsPublicKey, + pub proxy_bls: Vec<(BlsPublicKey, ModuleId)>, +} + #[derive(Clone, Debug)] pub struct DirkManager { chain: Chain, @@ -68,6 +75,7 @@ pub struct DirkManager { unlock: bool, secrets_path: PathBuf, proxy_store: Option, + proxy_maps: Vec, } impl DirkManager { @@ -142,31 +150,40 @@ impl DirkManager { a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) }) { let public_key = BlsPublicKey::try_from(dirk_account.public_key.as_slice())?; - let key_name = dirk_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + let key_name = + dirk_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); trace!(?dirk_account.name, "Adding account to hashmap"); - + let is_proxy = is_proxy_key_name(key_name); - accounts.insert(hex::encode(public_key), Account { - wallet: wallet.to_string(), - name: key_name.to_string(), - public_key: Some(public_key), - hosts: vec![HostInfo { server_name: server_name.clone(), participant_id: 1 }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy, - }); + accounts.insert( + hex::encode(public_key), + Account { + wallet: wallet.to_string(), + name: key_name.to_string(), + public_key: Some(public_key), + hosts: vec![HostInfo { + server_name: server_name.clone(), + participant_id: 1, + }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy, + }, + ); } // Handle distributed accounts for dist_account in dirk_distributed_accounts.iter().filter(|a| { a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) }) { - let public_key = BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; - let key_name = dist_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); - + let public_key = + BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; + let key_name = + dist_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + let is_proxy = is_proxy_key_name(key_name); - + trace!(?dist_account.name, "Adding distributed account to hashmap"); // Find the participant ID for this host from the participants list @@ -186,16 +203,20 @@ impl DirkManager { .entry(hex::encode(public_key)) .and_modify(|account| { if !account.hosts.iter().any(|host| host.server_name == server_name) { - account - .hosts - .push(HostInfo { server_name: server_name.clone(), participant_id }); + account.hosts.push(HostInfo { + server_name: server_name.clone(), + participant_id, + }); } }) .or_insert_with(|| Account { wallet: wallet.to_string(), name: key_name.to_string(), public_key: Some(public_key), - hosts: vec![HostInfo { server_name: server_name.clone(), participant_id }], + hosts: vec![HostInfo { + server_name: server_name.clone(), + participant_id, + }], wallet_type: WalletType::Distributed, signing_threshold: dist_account.signing_threshold, is_proxy, @@ -204,14 +225,18 @@ impl DirkManager { } } - Ok(Self { + let mut manager = Self { chain, channels, accounts, unlock: config.unlock, secrets_path: config.secrets_path, proxy_store: None, - }) + proxy_maps: vec![], + }; + manager.build_consensus_proxy_map(); + + Ok(manager) } fn accounts(&self) -> Vec { @@ -229,24 +254,20 @@ impl DirkManager { /// Get the complete account name (`wallet/account`) for a public key. /// Returns `Ok(None)` if the account was not found. /// Returns `Err` if there was a communication error with Dirk. - fn get_pubkey_account( - &self, - pubkey: BlsPublicKey, - ) -> Option { + fn get_pubkey_account(&self, pubkey: BlsPublicKey) -> Option { match self .accounts() .iter() - .find(|account| - account.public_key.is_some_and(|account_pk| account_pk == pubkey)) { - Some(account) => Some(account.complete_name()), - None => None, - } + .find(|account| account.public_key.is_some_and(|account_pk| account_pk == pubkey)) + { + Some(account) => Some(account.complete_name()), + None => None, + } } /// Returns the public keys of the config-registered accounts pub async fn consensus_pubkeys(&self) -> Vec { - self - .accounts_non_proxy() + self.accounts_non_proxy() .iter() .filter_map(|account| account.public_key) .collect::>() @@ -257,54 +278,78 @@ impl DirkManager { /// `consensus_account/module_id/uuid`, where `consensus_account` is the /// name of a config-registered account. pub async fn proxies(&self) -> Vec { - self - .accounts() + self.accounts() .iter() .filter(|account| account.is_proxy) .filter_map(|account| account.public_key) .collect::>() } - /// Returns a mapping of the proxy accounts' pubkeys by consensus account, + pub fn get_consensus_proxy_maps(&self, module_id: &ModuleId) -> Vec { + dbg!(&self.proxy_maps); + + // Filter only proxy keys whose module_id match the request one + // and also remove the module_id part from the struct, since the response + // does not include it. + self.proxy_maps + .iter() + .map(|map| { + let filtered_proxy_bls: Vec = map + .proxy_bls + .iter() + .filter_map( + |(pubkey, id)| { + if id == module_id { + Some(pubkey.clone()) + } else { + None + } + }, + ) + .collect(); + + ConsensusProxyMap { + consensus: map.consensus.clone(), + proxy_bls: filtered_proxy_bls, + proxy_ecdsa: vec![], + } + }) + .collect() + } + + /// Builds a mapping of the proxy accounts' pubkeys by consensus account, /// for a given module. /// An account is considered a proxy if its name has the format /// `consensus_account/module_id/uuid`, where `consensus_account` is the /// name of a config-registered account. - pub async fn get_consensus_proxy_maps( - &self, - module_id: &ModuleId, - ) -> Result, SignerModuleError> { + pub fn build_consensus_proxy_map(&mut self) { let consensus_accounts = self.accounts_non_proxy(); let accounts = self.accounts(); - let proxy_accounts: Vec<_> = accounts - .iter() - .filter(|account| account.is_proxy) - .collect(); + let proxy_accounts: Vec<_> = accounts.iter().filter(|account| account.is_proxy).collect(); - let mut proxy_maps = Vec::new(); for consensus_account in consensus_accounts { let Some(consensus_key) = consensus_account.public_key else { - trace!("get_consensus_proxy_maps: skipping"); continue; }; - let mut proxy_keys: Vec = vec![]; - let start_of_proxy_name = format!("{}/{module_id}", consensus_account.complete_name()); + let mut proxy_bls: Vec<(BlsPublicKey, ModuleId)> = vec![]; + + let start_of_proxy_name = format!("{}/", consensus_account.complete_name()); for proxy in &proxy_accounts { - trace!(%proxy.name, %start_of_proxy_name, "get_consensus_proxy_maps: checking if name starts with"); - if proxy.complete_name().starts_with(&start_of_proxy_name) { + if proxy.complete_name().starts_with(&start_of_proxy_name) + && is_proxy_key_name(&proxy.name) + { if let Some(pubkey) = proxy.public_key { - proxy_keys.push(pubkey); + // Extract module_id from the proxy account's complete name + let module_id = ModuleId( + proxy.complete_name().split('/').nth(2).unwrap_or_default().to_string(), + ); + proxy_bls.push((pubkey, module_id)); } } } - proxy_maps.push(ConsensusProxyMap { - consensus: consensus_key, - proxy_bls: proxy_keys, - proxy_ecdsa: vec![], - }); - } - Ok(proxy_maps) + self.proxy_maps.push(ModuleConsensusProxyMap { consensus: consensus_key, proxy_bls }); + } } /// Generate a random password of 64 hex-characters @@ -513,18 +558,23 @@ impl DirkManager { String::from(account_name.split_once("/").map(|(_, n)| n).unwrap_or_default()); let delegation = self - .insert_proxy_account(proxy_key, consensus_pubkey, module_id, Account { - wallet: consensus_account.wallet.clone(), - name: account_name_without_wallet.clone(), - public_key: Some(proxy_key), - hosts: vec![HostInfo { - server_name: first_host.server_name.clone(), - participant_id: first_host.participant_id, - }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy: true, - }) + .insert_proxy_account( + proxy_key, + consensus_pubkey, + module_id, + Account { + wallet: consensus_account.wallet.clone(), + name: account_name_without_wallet.clone(), + public_key: Some(proxy_key), + hosts: vec![HostInfo { + server_name: first_host.server_name.clone(), + participant_id: first_host.participant_id, + }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy: true, + }, + ) .await?; // Unlock the account for immediate use @@ -547,7 +597,10 @@ impl DirkManager { )) })?; - trace!(host = host.server_name, "Sending generate request for distributed proxy key"); + trace!( + host = host.server_name, + "Sending generate request for distributed proxy key" + ); let proxy_key = make_generate_proxy_request( GenerateRequest { account: account_name.clone(), @@ -563,15 +616,20 @@ impl DirkManager { let consensus_name = consensus_account_info.name; let delegation = self - .insert_proxy_account(proxy_key, consensus_pubkey, module_id.clone(), Account { - wallet: consensus_account_info.wallet.clone(), - name: format!("{consensus_name}/{module_id}/{uuid}"), - public_key: Some(proxy_key), - hosts: consensus_account_info.hosts.clone(), - wallet_type: WalletType::Distributed, - signing_threshold: consensus_account_info.signing_threshold, - is_proxy: true, - }) + .insert_proxy_account( + proxy_key, + consensus_pubkey, + module_id.clone(), + Account { + wallet: consensus_account_info.wallet.clone(), + name: format!("{consensus_name}/{module_id}/{uuid}"), + public_key: Some(proxy_key), + hosts: consensus_account_info.hosts.clone(), + wallet_type: WalletType::Distributed, + signing_threshold: consensus_account_info.signing_threshold, + is_proxy: true, + }, + ) .await?; Ok(delegation) @@ -638,7 +696,7 @@ impl DirkManager { } let mut set = JoinSet::new(); - + // Spawn tasks for each host for host in &account.hosts { let Some(channel) = self.channels.get(&host.server_name).cloned() else { @@ -679,8 +737,11 @@ impl DirkManager { while let Some(result) = set.join_next().await { // Check if we already have enough signatures before processing more if signatures.len() >= num_hosts_needed { - trace!("Already have enough signatures ({}/{}), cancelling remaining tasks", - signatures.len(), num_hosts_needed); + trace!( + "Already have enough signatures ({}/{}), cancelling remaining tasks", + signatures.len(), + num_hosts_needed + ); set.abort_all(); break; } diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index 87aee2af..e7d3bf19 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -48,7 +48,7 @@ impl SigningManager { local_manager.read().await.get_consensus_proxy_maps(module_id) } SigningManager::Dirk(dirk_manager) => { - dirk_manager.get_consensus_proxy_maps(module_id).await + Ok(dirk_manager.get_consensus_proxy_maps(module_id)) } } } From 96e4df711db43f91027b572b856ea865b7b79588 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Fri, 21 Feb 2025 12:16:02 -0300 Subject: [PATCH 068/104] Dirk: Disallow creating proxies via a proxy key, and signing with a proxy but using consensus type. --- crates/signer/src/manager/dirk.rs | 52 ++++++++++++++++++------------- crates/signer/src/service.rs | 4 +-- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 4898bfd8..c046fd42 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -234,7 +234,7 @@ impl DirkManager { self.accounts.values().cloned().collect() } - fn accounts_non_proxy(&self) -> Vec { + fn consensus_accounts(&self) -> Vec { self.accounts.values().filter(|a| !a.is_proxy).cloned().collect() } @@ -242,19 +242,17 @@ impl DirkManager { Ok(Self { proxy_store: Some(proxy_store), ..self }) } - /// Get the complete account name (`wallet/account`) for a public key. - /// Returns `Ok(None)` if the account was not found. - /// Returns `Err` if there was a communication error with Dirk. - fn get_pubkey_account(&self, pubkey: BlsPublicKey) -> Option { - self.accounts() - .iter() - .find(|account| account.public_key.is_some_and(|account_pk| account_pk == pubkey)) - .map(|account| account.complete_name()) + fn get_consensus_account(&self, pubkey: BlsPublicKey) -> Option { + self.accounts.get(&hex::encode(pubkey)).filter(|acc| !acc.is_proxy).cloned() + } + + fn get_proxy_account(&self, pubkey: BlsPublicKey) -> Option { + self.accounts.get(&hex::encode(pubkey)).filter(|acc| acc.is_proxy).cloned() } /// Returns the public keys of the config-registered accounts pub async fn consensus_pubkeys(&self) -> Vec { - self.accounts_non_proxy() + self.consensus_accounts() .iter() .filter_map(|account| account.public_key) .collect::>() @@ -302,11 +300,10 @@ impl DirkManager { /// `consensus_account/module_id/uuid`, where `consensus_account` is the /// name of a config-registered account. pub fn build_consensus_proxy_map(&mut self) { - let consensus_accounts = self.accounts_non_proxy(); let accounts = self.accounts(); let proxy_accounts: Vec<_> = accounts.iter().filter(|account| account.is_proxy).collect(); - for consensus_account in consensus_accounts { + for consensus_account in self.consensus_accounts() { let Some(consensus_key) = consensus_account.public_key else { continue; }; @@ -488,9 +485,16 @@ impl DirkManager { module_id: ModuleId, consensus_pubkey: BlsPublicKey, ) -> Result, SignerModuleError> { - let consensus_account = self - .get_pubkey_account(consensus_pubkey) - .ok_or(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; + let Some(consensus_account) = self + .consensus_accounts() + .iter() + .find(|account| { + account.public_key.is_some_and(|account_pk| account_pk == consensus_pubkey) + }) + .map(|account| account.complete_name()) + else { + return Err(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec())); + }; let consensus_account_info = self .accounts() @@ -622,7 +626,7 @@ impl DirkManager { // Get delegation signature from the consensus account let message = ProxyDelegation { delegator, proxy: proxy_key }; - let signature = self.request_signature(delegator, message.tree_hash_root().0).await?; + let signature = self.request_signature(delegator, message.tree_hash_root().0, true).await?; let delegation: SignedProxyDelegation = SignedProxyDelegation { message, signature }; @@ -639,14 +643,18 @@ impl DirkManager { &self, pubkey: BlsPublicKey, object_root: [u8; 32], + is_consensus: bool, ) -> Result { let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); - let key = hex::encode(pubkey); - let account = self - .accounts - .get(&key) - .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))? - .clone(); + let account = if is_consensus { + self.get_consensus_account(pubkey) + .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))? + .clone() + } else { + self.get_proxy_account(pubkey) + .ok_or_else(|| SignerModuleError::UnknownProxySigner(pubkey.to_vec()))? + .clone() + }; match account.wallet_type { WalletType::Simple => { diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 5c10a5e8..b3cbba13 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -186,12 +186,12 @@ async fn handle_request_signature( }, SigningManager::Dirk(dirk_manager) => match request { SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => dirk_manager - .request_signature(pubkey, object_root) + .request_signature(pubkey, object_root, true) .await .map(|sig| Json(sig).into_response()), SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { dirk_manager - .request_signature(bls_key, object_root) + .request_signature(bls_key, object_root, false) .await .map(|sig| Json(sig).into_response()) } From a149ea70c9c119e23ef987d7302bdf67b555825b Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Fri, 21 Feb 2025 12:39:38 -0300 Subject: [PATCH 069/104] Dirk: Remove mutex from LocalSigningManager inside SigningManager, since it's already outside the struct. We had the same for DirkManager, this fixes some clippy errors. --- crates/common/src/signer/types.rs | 2 +- crates/signer/src/manager/local.rs | 1 + crates/signer/src/manager/mod.rs | 14 ++++---------- crates/signer/src/service.rs | 12 +----------- 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/crates/common/src/signer/types.rs b/crates/common/src/signer/types.rs index 4071f858..a5888fdb 100644 --- a/crates/common/src/signer/types.rs +++ b/crates/common/src/signer/types.rs @@ -34,7 +34,7 @@ pub struct EcdsaProxySigner { pub delegation: SignedProxyDelegationEcdsa, } -#[derive(Default)] +#[derive(Default, Clone)] pub struct ProxySigners { pub bls_signers: HashMap, pub ecdsa_signers: HashMap, diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index 0f9ded3b..e321ed92 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -16,6 +16,7 @@ use tree_hash::TreeHash; use crate::error::SignerModuleError; +#[derive(Clone)] pub struct LocalSigningManager { chain: Chain, proxy_store: Option, diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index e7d3bf19..62547cb1 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -1,9 +1,6 @@ -use std::sync::Arc; - use cb_common::{commit::request::ConsensusProxyMap, types::ModuleId}; use dirk::DirkManager; use local::LocalSigningManager; -use tokio::sync::RwLock; use crate::error::SignerModuleError; @@ -12,7 +9,7 @@ pub mod local; #[derive(Clone)] pub enum SigningManager { - Local(Arc>), + Local(LocalSigningManager), Dirk(DirkManager), } @@ -20,9 +17,7 @@ impl SigningManager { /// Amount of consensus signers available pub async fn available_consensus_signers(&self) -> eyre::Result { match self { - SigningManager::Local(local_manager) => { - Ok(local_manager.read().await.consensus_pubkeys().len()) - } + SigningManager::Local(local_manager) => Ok(local_manager.consensus_pubkeys().len()), SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.consensus_pubkeys().await.len()), } } @@ -31,8 +26,7 @@ impl SigningManager { pub async fn available_proxy_signers(&self) -> eyre::Result { match self { SigningManager::Local(local_manager) => { - let manager = local_manager.read().await; - let proxies = manager.proxies(); + let proxies = local_manager.proxies(); Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) } SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.proxies().await.len()), @@ -45,7 +39,7 @@ impl SigningManager { ) -> Result, SignerModuleError> { match self { SigningManager::Local(local_manager) => { - local_manager.read().await.get_consensus_proxy_maps(module_id) + local_manager.get_consensus_proxy_maps(module_id) } SigningManager::Dirk(dirk_manager) => { Ok(dirk_manager.get_consensus_proxy_maps(module_id)) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index b3cbba13..be68db21 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -162,23 +162,17 @@ async fn handle_request_signature( let res = match &*manager { SigningManager::Local(local_manager) => match request { SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => local_manager - .read() - .await .sign_consensus(&pubkey, &object_root) .await .map(|sig| Json(sig).into_response()), SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { local_manager - .read() - .await .sign_proxy_bls(&bls_key, &object_root) .await .map(|sig| Json(sig).into_response()) } SignRequest::ProxyEcdsa(SignProxyRequest { object_root, pubkey: ecdsa_key }) => { local_manager - .read() - .await .sign_proxy_ecdsa(&ecdsa_key, &object_root) .await .map(|sig| Json(sig).into_response()) @@ -227,14 +221,10 @@ async fn handle_generate_proxy( let res = match &mut *manager { SigningManager::Local(local_manager) => match request.scheme { EncryptionScheme::Bls => local_manager - .write() - .await .create_proxy_bls(module_id.clone(), request.consensus_pubkey) .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), EncryptionScheme::Ecdsa => local_manager - .write() - .await .create_proxy_ecdsa(module_id.clone(), request.consensus_pubkey) .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), @@ -312,7 +302,7 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result Date: Fri, 21 Feb 2025 12:39:38 -0300 Subject: [PATCH 070/104] Dirk: chore: remove dbg! print --- crates/common/src/signer/types.rs | 2 +- crates/signer/src/manager/dirk.rs | 2 -- crates/signer/src/manager/local.rs | 1 + crates/signer/src/manager/mod.rs | 14 ++++---------- crates/signer/src/service.rs | 12 +----------- 5 files changed, 7 insertions(+), 24 deletions(-) diff --git a/crates/common/src/signer/types.rs b/crates/common/src/signer/types.rs index 4071f858..a5888fdb 100644 --- a/crates/common/src/signer/types.rs +++ b/crates/common/src/signer/types.rs @@ -34,7 +34,7 @@ pub struct EcdsaProxySigner { pub delegation: SignedProxyDelegationEcdsa, } -#[derive(Default)] +#[derive(Default, Clone)] pub struct ProxySigners { pub bls_signers: HashMap, pub ecdsa_signers: HashMap, diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index c046fd42..93e30f84 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -271,8 +271,6 @@ impl DirkManager { } pub fn get_consensus_proxy_maps(&self, module_id: &ModuleId) -> Vec { - dbg!(&self.proxy_maps); - // Filter only proxy keys whose module_id match the request one // and also remove the module_id part from the struct, since the response // does not include it. diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index 0f9ded3b..e321ed92 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -16,6 +16,7 @@ use tree_hash::TreeHash; use crate::error::SignerModuleError; +#[derive(Clone)] pub struct LocalSigningManager { chain: Chain, proxy_store: Option, diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index e7d3bf19..62547cb1 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -1,9 +1,6 @@ -use std::sync::Arc; - use cb_common::{commit::request::ConsensusProxyMap, types::ModuleId}; use dirk::DirkManager; use local::LocalSigningManager; -use tokio::sync::RwLock; use crate::error::SignerModuleError; @@ -12,7 +9,7 @@ pub mod local; #[derive(Clone)] pub enum SigningManager { - Local(Arc>), + Local(LocalSigningManager), Dirk(DirkManager), } @@ -20,9 +17,7 @@ impl SigningManager { /// Amount of consensus signers available pub async fn available_consensus_signers(&self) -> eyre::Result { match self { - SigningManager::Local(local_manager) => { - Ok(local_manager.read().await.consensus_pubkeys().len()) - } + SigningManager::Local(local_manager) => Ok(local_manager.consensus_pubkeys().len()), SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.consensus_pubkeys().await.len()), } } @@ -31,8 +26,7 @@ impl SigningManager { pub async fn available_proxy_signers(&self) -> eyre::Result { match self { SigningManager::Local(local_manager) => { - let manager = local_manager.read().await; - let proxies = manager.proxies(); + let proxies = local_manager.proxies(); Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) } SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.proxies().await.len()), @@ -45,7 +39,7 @@ impl SigningManager { ) -> Result, SignerModuleError> { match self { SigningManager::Local(local_manager) => { - local_manager.read().await.get_consensus_proxy_maps(module_id) + local_manager.get_consensus_proxy_maps(module_id) } SigningManager::Dirk(dirk_manager) => { Ok(dirk_manager.get_consensus_proxy_maps(module_id)) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index b3cbba13..be68db21 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -162,23 +162,17 @@ async fn handle_request_signature( let res = match &*manager { SigningManager::Local(local_manager) => match request { SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => local_manager - .read() - .await .sign_consensus(&pubkey, &object_root) .await .map(|sig| Json(sig).into_response()), SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { local_manager - .read() - .await .sign_proxy_bls(&bls_key, &object_root) .await .map(|sig| Json(sig).into_response()) } SignRequest::ProxyEcdsa(SignProxyRequest { object_root, pubkey: ecdsa_key }) => { local_manager - .read() - .await .sign_proxy_ecdsa(&ecdsa_key, &object_root) .await .map(|sig| Json(sig).into_response()) @@ -227,14 +221,10 @@ async fn handle_generate_proxy( let res = match &mut *manager { SigningManager::Local(local_manager) => match request.scheme { EncryptionScheme::Bls => local_manager - .write() - .await .create_proxy_bls(module_id.clone(), request.consensus_pubkey) .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), EncryptionScheme::Ecdsa => local_manager - .write() - .await .create_proxy_ecdsa(module_id.clone(), request.consensus_pubkey) .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), @@ -312,7 +302,7 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result Date: Fri, 21 Feb 2025 15:03:08 -0300 Subject: [PATCH 071/104] Dirk: Use urls instead of host names in channel map --- crates/signer/src/manager/dirk.rs | 95 ++++++++++++------------------- 1 file changed, 37 insertions(+), 58 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 93e30f84..c6100f60 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -34,7 +34,7 @@ enum WalletType { #[derive(Clone, Debug)] struct HostInfo { - server_name: String, + url: String, participant_id: u64, } @@ -99,8 +99,8 @@ impl DirkManager { let mut channels = HashMap::new(); for (i, tls_config) in tls_configs.iter().enumerate() { let config_host = config.hosts[i].clone(); - let server_name = config_host.server_name.unwrap_or_default(); - match Channel::from_shared(config_host.url.to_string()) + let url = config_host.url.to_string(); + match Channel::from_shared(url.clone()) .map_err(|_| eyre::eyre!("Invalid Dirk URL"))? .tls_config(tls_config.clone()) .map_err(|_| eyre::eyre!("Invalid Dirk TLS config"))? @@ -108,10 +108,10 @@ impl DirkManager { .await { Ok(ch) => { - channels.insert(server_name, ch); + channels.insert(url.clone(), ch); } Err(e) => { - trace!("Couldn't connect to Dirk with server name {}: {e}", server_name); + trace!("Couldn't connect to Dirk host {}: {e}", url); } } } @@ -119,10 +119,10 @@ impl DirkManager { let mut accounts: HashMap = HashMap::new(); for host in config.hosts { - let server_name = host.server_name.unwrap_or_default(); + let url = host.url.to_string(); let channel = channels - .get(&server_name) - .ok_or(eyre::eyre!("Couldn't connect to Dirk with server name {server_name}"))? + .get(&url) + .ok_or(eyre::eyre!("Couldn't connect to Dirk host {url}"))? .clone(); let (dirk_accounts, dirk_distributed_accounts) = get_accounts_in_wallets( @@ -154,10 +154,7 @@ impl DirkManager { wallet: wallet.to_string(), name: key_name.to_string(), public_key: Some(public_key), - hosts: vec![HostInfo { - server_name: server_name.clone(), - participant_id: 1, - }], + hosts: vec![HostInfo { url: url.clone(), participant_id: 1 }], wallet_type: WalletType::Simple, signing_threshold: 1, is_proxy, @@ -172,31 +169,31 @@ impl DirkManager { BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; let key_name = dist_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); - let is_proxy = is_proxy_key_name(key_name); trace!(?dist_account.name, "Adding distributed account to hashmap"); // Find the participant ID for this host from the participants list - let participant_id = dist_account - .participants - .iter() - .find(|p| p.name == server_name) - .map(|p| p.id) - .ok_or_else(|| { + let url_hostname = host + .url + .host_str() + .ok_or_else(|| eyre::eyre!("Invalid Dirk host {}", host.url))?; + trace!(%url_hostname, "url_hostname"); + let current_participant = + dist_account.participants.first().ok_or_else(|| { eyre::eyre!( "Host {} not found in distributed account participants", - server_name + url ) })?; accounts .entry(hex::encode(public_key)) .and_modify(|account| { - if !account.hosts.iter().any(|host| host.server_name == server_name) { + if !account.hosts.iter().any(|host| host.url == url) { account.hosts.push(HostInfo { - server_name: server_name.clone(), - participant_id, + url: url.clone(), + participant_id: current_participant.id, }); } }) @@ -205,8 +202,8 @@ impl DirkManager { name: key_name.to_string(), public_key: Some(public_key), hosts: vec![HostInfo { - server_name: server_name.clone(), - participant_id, + url: url.clone(), + participant_id: current_participant.id, }], wallet_type: WalletType::Distributed, signing_threshold: dist_account.signing_threshold, @@ -380,7 +377,7 @@ impl DirkManager { // Try to find any available host's channel for host in &account.hosts { - if let Some(channel) = self.channels.get(&host.server_name) { + if let Some(channel) = self.channels.get(&host.url) { return Ok(channel.clone()); } } @@ -404,14 +401,8 @@ impl DirkManager { WalletType::Distributed => { // For distributed accounts, unlock on all hosts for host in &account_entry.hosts { - if let Some(channel) = self.channels.get(&host.server_name) { - self.unlock_account_on_channel( - channel, - &account, - &password, - Some(&host.server_name), - ) - .await?; + if let Some(channel) = self.channels.get(&host.url) { + self.unlock_account_on_channel(channel, &account, &password).await?; } } } @@ -421,7 +412,7 @@ impl DirkManager { self.get_channel_for_pubkey(account_entry.public_key.as_ref().ok_or_else( || SignerModuleError::Internal("Account has no public key".to_string()), )?)?; - self.unlock_account_on_channel(&channel, &account, &password, None).await?; + self.unlock_account_on_channel(&channel, &account, &password).await?; } } Ok(()) @@ -433,9 +424,8 @@ impl DirkManager { channel: &Channel, account: &str, password: &str, - host_domain: Option<&str>, ) -> Result<(), SignerModuleError> { - trace!(account, host = host_domain, "unlock_account_on_channel"); + trace!(account, "unlock_account_on_channel"); const MAX_RETRIES: u32 = 5; let mut retry_count = 0; @@ -466,12 +456,7 @@ impl DirkManager { ))); } - warn!( - ?status, - retry_count, - host = host_domain, - "Connection failed, retrying in 3 seconds..." - ); + warn!(?status, retry_count, "Connection failed, retrying in 3 seconds..."); tokio::time::sleep(std::time::Duration::from_secs(3)).await; } } @@ -544,7 +529,7 @@ impl DirkManager { name: account_name_without_wallet.clone(), public_key: Some(proxy_key), hosts: vec![HostInfo { - server_name: first_host.server_name.clone(), + url: first_host.url.clone(), participant_id: first_host.participant_id, }], wallet_type: WalletType::Simple, @@ -566,17 +551,11 @@ impl DirkManager { "No hosts available for consensus account".to_string(), ) })?; - let channel = self.channels.get(&host.server_name).cloned().ok_or_else(|| { - SignerModuleError::Internal(format!( - "No channel found for host {}", - host.server_name - )) + let channel = self.channels.get(&host.url).cloned().ok_or_else(|| { + SignerModuleError::Internal(format!("No channel found for host {}", host.url)) })?; - trace!( - host = host.server_name, - "Sending generate request for distributed proxy key" - ); + trace!(host = host.url, "Sending generate request for distributed proxy key"); let proxy_key = make_generate_proxy_request( GenerateRequest { account: account_name.clone(), @@ -674,16 +653,16 @@ impl DirkManager { // Spawn tasks for each host for host in &account.hosts { - let Some(channel) = self.channels.get(&host.server_name).cloned() else { + let Some(channel) = self.channels.get(&host.url).cloned() else { continue; }; let dirk = self.clone(); let account = account.clone(); - let server_name = host.server_name.clone(); + let server_url = host.url.clone(); let participant_id = host.participant_id; set.spawn(async move { - trace!(host = server_name, "Requesting signature shard for creating proxy"); + trace!(host = server_url, "Requesting signature shard for creating proxy"); match dirk .sign_with_channel(channel, pubkey, &account, domain, object_root) @@ -691,14 +670,14 @@ impl DirkManager { { Ok(signature) => { trace!( - host = server_name, + host = server_url, participant_id = participant_id, "Got signature shard" ); Ok((signature, participant_id)) } Err(e) => { - warn!("Failed to get signature from host {}: {}", server_name, e); + warn!("Failed to get signature from host {}: {}", server_url, e); Err(e) } } From ba587e52e62a7688f5b2c7bb9f8b5a47748fc59a Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Tue, 25 Feb 2025 15:01:17 -0300 Subject: [PATCH 072/104] Adjust spacing in config.example.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel Iñaki Bilbao --- config.example.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config.example.toml b/config.example.toml index d24cb7f2..54015675 100644 --- a/config.example.toml +++ b/config.example.toml @@ -165,19 +165,19 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # unlock = false # Add one entry like this for each Dirk host -#[[signer.dirk.hosts]] +# [[signer.dirk.hosts]] # Domain name of the server to use in TLS verification, if different from the URL # OPTIONAL -#server_name = "localhost-1" +# server_name = "localhost-1" # Complete URL of a Dirk gateway -#url = "https://localhost:8881" +# url = "https://localhost:8881" # Accounts to use as consensus keys -#accounts = ["Wallet1/Account1", "DistributedWallet/Account1"] +# accounts = ["Wallet1/Account1", "DistributedWallet/Account1"] -#[[signer.dirk.hosts]] -#server_name = "localhost-2" -#url = "https://localhost:8882" -#accounts = ["Wallet2/Account2", "DistributedWallet/Account1"] +# [[signer.dirk.hosts]] +# server_name = "localhost-2" +# url = "https://localhost:8882" +# accounts = ["Wallet2/Account2", "DistributedWallet/Account1"] # Configuration for how the Signer module should store proxy delegations. # OPTIONAL From a6a8ee057aad1e08a51aae11ccd1978365794d9a Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Tue, 25 Feb 2025 15:01:38 -0300 Subject: [PATCH 073/104] Avoid allocating a vector unnecesarily MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel Iñaki Bilbao --- crates/signer/src/manager/dirk.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index c6100f60..95165a65 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -910,9 +910,7 @@ async fn make_generate_proxy_request( /// Checks if a key name follows the proxy pattern /// // fn is_proxy_key_name(key_name: &str) -> bool { - key_name.split('/').count() == 3 && { - let parts: Vec<&str> = key_name.split('/').collect(); - uuid::Uuid::parse_str(parts[2]).is_ok() // Verify the last part is a - // valid UUID + key_name.split('/').count() == 3 && + uuid::Uuid::parse_str(key_name.split('/').last().unwrap_or_default()).is_ok() } } From 3bda87a4dc01f0c585d21d53dc2f8d7b8f3298dd Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Tue, 25 Feb 2025 15:01:53 -0300 Subject: [PATCH 074/104] Change names in closure for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel Iñaki Bilbao --- crates/signer/src/manager/dirk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 95165a65..39af4b4a 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -145,7 +145,7 @@ impl DirkManager { }) { let public_key = BlsPublicKey::try_from(dirk_account.public_key.as_slice())?; let key_name = - dirk_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + dirk_account.name.split_once("/").map(|(_wallet, name)| name).unwrap_or_default(); trace!(?dirk_account.name, "Adding account to hashmap"); let is_proxy = is_proxy_key_name(key_name); From 40a71b1b6b1610e27b21e48befbf48e6a90500ab Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Tue, 25 Feb 2025 15:02:28 -0300 Subject: [PATCH 075/104] Change names in closure for clarity (2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel Iñaki Bilbao --- crates/signer/src/manager/dirk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 39af4b4a..8193c4b0 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -168,7 +168,7 @@ impl DirkManager { let public_key = BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; let key_name = - dist_account.name.split_once("/").map(|(_, n)| n).unwrap_or_default(); + dist_account.name.split_once("/").map(|(_wallet, name)| name).unwrap_or_default(); let is_proxy = is_proxy_key_name(key_name); trace!(?dist_account.name, "Adding distributed account to hashmap"); From 71e05f500b4e6925257e72452172a60138153bda Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Tue, 25 Feb 2025 15:29:53 -0300 Subject: [PATCH 076/104] Remove signer_name from examples/configs/dirk_signer.toml --- examples/configs/dirk_signer.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml index e476b07b..32787107 100644 --- a/examples/configs/dirk_signer.toml +++ b/examples/configs/dirk_signer.toml @@ -17,7 +17,6 @@ unlock = true # Example of a single Dirk host [[signer.dirk.hosts]] -server_name = "gateway.dirk.url" url = "https://gateway.dirk.url" accounts = ["wallet1/accountX", "wallet2/accountY"] From c10b4dcc80ac62791e8023538fb3f59241e62462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 26 Feb 2025 18:52:18 +0000 Subject: [PATCH 077/104] Big refactor --- crates/common/src/config/signer.rs | 8 +- crates/signer/src/manager/dirk.rs | 1144 +++++++++------------------- crates/signer/src/service.rs | 6 +- 3 files changed, 387 insertions(+), 771 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index f950de4e..5396c76c 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -34,10 +34,10 @@ fn default_signer() -> String { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] -pub struct DirkHost { +pub struct DirkHostConfig { /// Domain name of the server to use in TLS verification pub server_name: Option, - /// Complete URL of a Dirk gateway + /// Complete URL of the Dirk server pub url: Url, /// Accounts used as consensus keys pub accounts: Vec, @@ -61,7 +61,7 @@ pub enum SignerType { /// Dirk remote signer module Dirk { /// List of Dirk hosts with their accounts - hosts: Vec, + hosts: Vec, /// Path to the client certificate cert_path: PathBuf, /// Path to the client key @@ -80,7 +80,7 @@ pub enum SignerType { #[derive(Clone, Debug)] pub struct DirkConfig { - pub hosts: Vec, + pub hosts: Vec, pub client_cert: Identity, pub secrets_path: PathBuf, pub cert_auth: Option, diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 523caa37..a0f3b101 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -1,869 +1,512 @@ -use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf}; +use std::collections::HashMap; -use alloy::{hex, primitives::FixedBytes, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN}; +use alloy::{ + hex, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN, transports::http::reqwest::Url, +}; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, - config::DirkConfig, + config::{DirkConfig, DirkHostConfig}, constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, signer::{BlsPublicKey, BlsSignature, ProxyStore}, types::{Chain, ModuleId}, }; -use rand::Rng; -use tokio::task::JoinSet; -use tonic::transport::{Channel, ClientTlsConfig}; -use tracing::{info, trace, warn}; -use tree_hash::TreeHash; +use eyre::bail; +use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; +use tracing::{debug, error, info, warn}; use crate::{ - error::SignerModuleError::{self, DirkCommunicationError}, + error::SignerModuleError, proto::v1::{ account_manager_client::AccountManagerClient, lister_client::ListerClient, - sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, - DistributedAccount, GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, - UnlockAccountRequest, + signer_client::SignerClient, GenerateRequest, ListAccountsRequest, ResponseState, + SignRequest, }, }; #[derive(Clone, Debug)] -enum WalletType { - Simple, - Distributed, +struct CertConfig { + ca: Option, + client: Identity, +} + +#[derive(Clone, Debug)] +enum Account { + Simple(SimpleAccount), + Distributed(DistributedAccount), } #[derive(Clone, Debug)] -struct HostInfo { - url: String, - participant_id: u64, +struct SimpleAccount { + public_key: BlsPublicKey, + server: Url, + wallet: String, + name: String, } #[derive(Clone, Debug)] -struct Account { +struct DistributedAccount { + composite_public_key: BlsPublicKey, + participants: HashMap, + threshold: u32, wallet: String, name: String, - public_key: Option, - hosts: Vec, - wallet_type: WalletType, - signing_threshold: u32, - is_proxy: bool, } impl Account { - pub fn complete_name(&self) -> String { - format!("{}/{}", self.wallet, self.name) + pub fn public_key(&self) -> BlsPublicKey { + match self { + Account::Simple(account) => account.public_key, + Account::Distributed(account) => account.composite_public_key, + } } } #[derive(Clone, Debug)] -struct ModuleConsensusProxyMap { - pub consensus: BlsPublicKey, - pub proxy_bls: Vec<(BlsPublicKey, ModuleId)>, +struct ProxyAccount { + consensus: Account, + module: ModuleId, + inner: Account, } #[derive(Clone, Debug)] pub struct DirkManager { chain: Chain, - channels: HashMap, // server_name -> channel - accounts: HashMap, // pubkey -> account - unlock: bool, - secrets_path: PathBuf, - proxy_store: Option, - proxy_maps: Vec, + certs: CertConfig, + connections: HashMap, + consensus_accounts: HashMap, + proxy_accounts: HashMap, } impl DirkManager { pub async fn new_from_config(chain: Chain, config: DirkConfig) -> eyre::Result { - let mut tls_configs = Vec::with_capacity(config.hosts.len()); - - // Create a TLS config for each host - for host in config.hosts.clone() { - let mut tls_config = ClientTlsConfig::new().identity(config.client_cert.clone()); - - if let Some(ca) = &config.cert_auth { - tls_config = tls_config.ca_certificate(ca.clone()); - } else { - trace!(?host.server_name, "CA certificate for server name not found"); - } + let certs = CertConfig { ca: config.cert_auth, client: config.client_cert }; - if let Some(server_name) = host.server_name.clone() { - tls_config = tls_config.domain_name(server_name); - tls_configs.push(tls_config); - } else { - trace!("Server name for host {0} not present", host.url); - } - } + let mut connections = HashMap::with_capacity(config.hosts.len()); + let mut consensus_accounts = HashMap::new(); - // Create a channel for each host, attempt to connect, - // and save it into a hashmap Host->Channel - let mut channels = HashMap::new(); - for (i, tls_config) in tls_configs.iter().enumerate() { - let config_host = config.hosts[i].clone(); - let url = config_host.url.to_string(); - match Channel::from_shared(url.clone()) - .map_err(|_| eyre::eyre!("Invalid Dirk URL"))? - .tls_config(tls_config.clone()) - .map_err(|_| eyre::eyre!("Invalid Dirk TLS config"))? - .connect() - .await - { - Ok(ch) => { - channels.insert(url.clone(), ch); - } + for host in config.hosts { + let channel = match connect(&host, &certs).await { + Ok(channel) => channel, Err(e) => { - trace!("Couldn't connect to Dirk host {}: {e}", url); + warn!("Failed to connect to Dirk host {}: {e}", host.url); + continue; } - } - } + }; - let mut accounts: HashMap = HashMap::new(); + connections.insert(host.url.clone(), channel.clone()); - for host in config.hosts { - let url = host.url.to_string(); - let channel = channels - .get(&url) - .ok_or(eyre::eyre!("Couldn't connect to Dirk host {url}"))? - .clone(); - - let (dirk_accounts, dirk_distributed_accounts) = get_accounts_in_wallets( - channel.clone(), - host.accounts - .iter() - .filter_map(|account| Some(account.split_once("/")?.0.to_string())) - .collect(), - ) - .await?; - - for account_name in host.accounts.clone() { - let (wallet, _) = account_name.split_once("/").ok_or(eyre::eyre!( - "Invalid account name: {account_name}. It must be in format wallet/account" - ))?; - - // Handle simple accounts - for dirk_account in dirk_accounts.iter().filter(|a| { - a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) - }) { - let public_key = BlsPublicKey::try_from(dirk_account.public_key.as_slice())?; - let key_name = - dirk_account.name.split_once("/").map(|(_wallet, name)| name).unwrap_or_default(); - trace!(?dirk_account.name, "Adding account to hashmap"); - - let is_proxy = is_proxy_key_name(key_name); - - accounts.insert(hex::encode(public_key), Account { - wallet: wallet.to_string(), - name: key_name.to_string(), - public_key: Some(public_key), - hosts: vec![HostInfo { url: url.clone(), participant_id: 1 }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy, - }); + for account_name in host.accounts { + let Ok((wallet, name)) = decompose_name(&account_name) else { + warn!("Invalid account name {account_name}"); + continue; + }; + + let response = match ListerClient::new(channel.clone()) + .list_accounts(ListAccountsRequest { paths: vec![account_name.clone()] }) + .await + { + Ok(res) => res, + Err(e) => { + warn!("Failed to get account {account_name}: {e}"); + continue; + } + }; + + if response.get_ref().state() != ResponseState::Succeeded { + warn!("Failed to get account {account_name}"); + continue; } - // Handle distributed accounts - for dist_account in dirk_distributed_accounts.iter().filter(|a| { - a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) - }) { - let public_key = - BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; - let key_name = - dist_account.name.split_once("/").map(|(_wallet, name)| name).unwrap_or_default(); - let is_proxy = is_proxy_key_name(key_name); - - trace!(?dist_account.name, "Adding distributed account to hashmap"); - - // Find the participant ID for this host from the participants list - let url_hostname = host - .url - .host_str() - .ok_or_else(|| eyre::eyre!("Invalid Dirk host {}", host.url))?; - trace!(%url_hostname, "url_hostname"); - let current_participant = - dist_account.participants.first().ok_or_else(|| { - eyre::eyre!( - "Host {} not found in distributed account participants", - url - ) - })?; - - accounts - .entry(hex::encode(public_key)) - .and_modify(|account| { - if !account.hosts.iter().any(|host| host.url == url) { - account.hosts.push(HostInfo { - url: url.clone(), - participant_id: current_participant.id, - }); - } - }) - .or_insert_with(|| Account { - wallet: wallet.to_string(), - name: key_name.to_string(), - public_key: Some(public_key), - hosts: vec![HostInfo { - url: url.clone(), - participant_id: current_participant.id, - }], - wallet_type: WalletType::Distributed, - signing_threshold: dist_account.signing_threshold, - is_proxy, - }); + if let Some(account) = response.get_ref().accounts.get(0) { + // The account is Simple + match BlsPublicKey::try_from(account.public_key.as_slice()) { + Ok(public_key) => { + consensus_accounts.insert( + public_key, + Account::Simple(SimpleAccount { + public_key, + server: host.url.clone(), + wallet: wallet.to_string(), + name: name.to_string(), + }), + ); + } + Err(_) => { + warn!("Failed to parse public key for account {account_name}"); + continue; + } + } + } else if let Some(account) = response.get_ref().distributed_accounts.get(0) { + // The account is Distributed + let Ok(public_key) = + BlsPublicKey::try_from(account.composite_public_key.as_slice()) + else { + warn!("Failed to parse composite public key for account {account_name}"); + continue; + }; + + match consensus_accounts.get_mut(&public_key) { + Some(Account::Distributed(DistributedAccount { participants, .. })) => { + participants.insert(participants.len() as u32 + 1, host.url.clone()); + } + Some(Account::Simple(_)) => { + bail!("Distributed public key already exists for simple account"); + } + None => { + let mut participants = + HashMap::with_capacity(account.participants.len()); + participants.insert(1, host.url.clone()); + consensus_accounts.insert( + public_key, + Account::Distributed(DistributedAccount { + composite_public_key: public_key, + participants, + threshold: account.signing_threshold, + wallet: wallet.to_string(), + name: name.to_string(), + }), + ); + } + } + } else { + warn!("Account {account_name} not found in server {}", host.url); } } } - let mut manager = Self { - chain, - channels, - accounts, - unlock: config.unlock, - secrets_path: config.secrets_path, - proxy_store: None, - proxy_maps: vec![], - }; - manager.build_consensus_proxy_map(); - - Ok(manager) - } - - fn accounts(&self) -> Vec { - self.accounts.values().cloned().collect() - } - - fn consensus_accounts(&self) -> Vec { - self.accounts.values().filter(|a| !a.is_proxy).cloned().collect() - } - - pub fn with_proxy_store(self, proxy_store: ProxyStore) -> eyre::Result { - Ok(Self { proxy_store: Some(proxy_store), ..self }) - } + debug!( + "Loaded {} consensus accounts: {}", + consensus_accounts.len(), + consensus_accounts.keys().map(|k| k.to_string()).collect::>().join(", ") + ); - fn get_consensus_account(&self, pubkey: BlsPublicKey) -> Option { - self.accounts.get(&hex::encode(pubkey)).filter(|acc| !acc.is_proxy).cloned() + Ok(Self { chain, certs, connections, consensus_accounts, proxy_accounts: HashMap::new() }) } - fn get_proxy_account(&self, pubkey: BlsPublicKey) -> Option { - self.accounts.get(&hex::encode(pubkey)).filter(|acc| acc.is_proxy).cloned() + // TODO + pub fn with_proxy_store(self, store: ProxyStore) -> eyre::Result { + Ok(self) } - /// Returns the public keys of the config-registered accounts pub async fn consensus_pubkeys(&self) -> Vec { - self.consensus_accounts() - .iter() - .filter_map(|account| account.public_key) - .collect::>() + self.consensus_accounts.keys().cloned().collect() } - /// Returns the public keys of all the proxy accounts found in Dirk. - /// An account is considered a proxy if its name has the format - /// `consensus_account/module_id/uuid`, where `consensus_account` is the - /// name of a config-registered account. - pub async fn proxies(&self) -> Vec { - self.accounts() - .iter() - .filter(|account| account.is_proxy) - .filter_map(|account| account.public_key) - .collect::>() + // TODO + pub async fn proxies(&self) -> Vec<()> { + Vec::new() } - pub fn get_consensus_proxy_maps(&self, module_id: &ModuleId) -> Vec { - // Filter only proxy keys whose module_id match the request one - // and also remove the module_id part from the struct, since the response - // does not include it. - self.proxy_maps + // TODO + pub fn get_consensus_proxy_maps(&self, module: &ModuleId) -> Vec { + self.consensus_accounts .iter() - .map(|map| { - let filtered_proxy_bls: Vec = map - .proxy_bls - .iter() - .filter_map(|(pubkey, id)| if id == module_id { Some(*pubkey) } else { None }) - .collect(); - - ConsensusProxyMap { - consensus: map.consensus, - proxy_bls: filtered_proxy_bls, - proxy_ecdsa: vec![], - } + .map(|(_, account)| ConsensusProxyMap { + consensus: account.public_key(), + proxy_bls: self + .proxy_accounts + .values() + .filter_map(|proxy| { + if proxy.module == *module && + proxy.consensus.public_key() == account.public_key() + { + Some(proxy.inner.public_key()) + } else { + None + } + }) + .collect(), + // ECDSA is not supported + proxy_ecdsa: Vec::new(), }) .collect() } - /// Builds a mapping of the proxy accounts' pubkeys by consensus account, - /// for a given module. - /// An account is considered a proxy if its name has the format - /// `consensus_account/module_id/uuid`, where `consensus_account` is the - /// name of a config-registered account. - pub fn build_consensus_proxy_map(&mut self) { - let accounts = self.accounts(); - let proxy_accounts: Vec<_> = accounts.iter().filter(|account| account.is_proxy).collect(); - - for consensus_account in self.consensus_accounts() { - let Some(consensus_key) = consensus_account.public_key else { - continue; - }; - let mut proxy_bls: Vec<(BlsPublicKey, ModuleId)> = vec![]; - - let start_of_proxy_name = format!("{}/", consensus_account.complete_name()); - for proxy in &proxy_accounts { - if proxy.complete_name().starts_with(&start_of_proxy_name) && - is_proxy_key_name(&proxy.name) - { - if let Some(pubkey) = proxy.public_key { - // Extract module_id from the proxy account's complete name - let module_id = ModuleId( - proxy.complete_name().split('/').nth(2).unwrap_or_default().to_string(), - ); - proxy_bls.push((pubkey, module_id)); - } - } + pub async fn request_consensus_signature( + &self, + pubkey: &BlsPublicKey, + object_root: [u8; 32], + ) -> Result { + match self.consensus_accounts.get(pubkey) { + Some(Account::Simple(account)) => { + self.request_simple_signature(account, object_root).await } - - self.proxy_maps.push(ModuleConsensusProxyMap { consensus: consensus_key, proxy_bls }); - } - } - - /// Generate a random password of 64 hex-characters - fn random_password() -> String { - let password_bytes: [u8; 32] = rand::thread_rng().gen(); - hex::encode(password_bytes) - } - - /// Read the password for an account from a file - fn read_password(&self, account: String) -> Result { - let path = self.secrets_path.join(format!("{account}.pass")); - trace!(path = ?path, "Reading password from file"); - fs::read_to_string(self.secrets_path.join(format!("{account}.pass"))).map_err(|err| { - SignerModuleError::Internal(format!( - "error reading password for account '{account}': {err}" - )) - }) - } - - /// Store the password for an account in a file - fn store_password(&self, account: String, password: String) -> Result<(), SignerModuleError> { - let account_dir = self - .secrets_path - .join( - account - .rsplit_once("/") - .ok_or(SignerModuleError::Internal(format!( - "account name '{account}' is invalid" - )))? - .0, - ) - .to_string_lossy() - .to_string(); - trace!(%account_dir, "Storing password in file"); - - fs::create_dir_all(account_dir.clone()).map_err(|err| { - SignerModuleError::Internal(format!("error creating dir '{account_dir}': {err}")) - })?; - fs::write(self.secrets_path.join(format!("{account}.pass")), password).map_err(|err| { - SignerModuleError::Internal(format!( - "error writing password for account '{account}': {err}" - )) - }) - } - - /// Get the associated channel for a public key - fn get_channel_for_pubkey(&self, pubkey: &BlsPublicKey) -> Result { - trace!(%pubkey, "Getting channel for public key"); - let key = hex::encode(pubkey); - let account = self - .accounts - .get(&key) - .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; - - // Try to find any available host's channel - for host in &account.hosts { - if let Some(channel) = self.channels.get(&host.url) { - return Ok(channel.clone()); + Some(Account::Distributed(account)) => { + self.request_distributed_signature(account, object_root).await } + None => Err(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec())), } - - Err(SignerModuleError::Internal("No available channel found for any host".to_string())) } - /// Unlock an account. For distributed accounts this is done for all it's - /// hosts. - async fn unlock_account( + pub async fn request_proxy_signature( &self, - account: String, - password: String, - ) -> Result<(), SignerModuleError> { - let account_entry = - self.accounts.values().find(|a| a.complete_name() == account).ok_or_else(|| { - SignerModuleError::Internal(format!("Account not found: {}", account)) - })?; - - match account_entry.wallet_type { - WalletType::Distributed => { - // For distributed accounts, unlock on all hosts - for host in &account_entry.hosts { - if let Some(channel) = self.channels.get(&host.url) { - self.unlock_account_on_channel(channel, &account, &password).await?; - } - } + pubkey: &BlsPublicKey, + object_root: [u8; 32], + ) -> Result { + match self.proxy_accounts.get(pubkey) { + Some(ProxyAccount { inner: Account::Simple(account), .. }) => { + self.request_simple_signature(account, object_root).await } - WalletType::Simple => { - // For simple accounts, unlock on a single host - let channel = - self.get_channel_for_pubkey(account_entry.public_key.as_ref().ok_or_else( - || SignerModuleError::Internal("Account has no public key".to_string()), - )?)?; - self.unlock_account_on_channel(&channel, &account, &password).await?; + Some(ProxyAccount { inner: Account::Distributed(account), .. }) => { + self.request_distributed_signature(account, object_root).await } + None => Err(SignerModuleError::UnknownProxySigner(pubkey.to_vec())), } - Ok(()) } - /// Unlock an account on a specific channel - async fn unlock_account_on_channel( + async fn request_simple_signature( &self, - channel: &Channel, - account: &str, - password: &str, - ) -> Result<(), SignerModuleError> { - trace!(account, "unlock_account_on_channel"); - const MAX_RETRIES: u32 = 5; - let mut retry_count = 0; - - loop { - let mut client = AccountManagerClient::new(channel.clone()); - let unlock_request = tonic::Request::new(UnlockAccountRequest { - account: account.to_string(), - passphrase: password.as_bytes().to_vec(), - }); - - match client.unlock(unlock_request).await { - Ok(unlock_response) => { - if unlock_response.get_ref().state() == ResponseState::Succeeded { - return Ok(()); - } - // We have connected but an error has been returned - let err = unlock_response.get_ref(); - warn!(?err, "unlock_account_on_channel error response"); - return Err(DirkCommunicationError( - "unlock_account_on_channel error response received".to_string(), - )); - } - Err(status) => { - retry_count += 1; - if retry_count >= MAX_RETRIES { - return Err(DirkCommunicationError(format!( - "Failed to connect after {MAX_RETRIES} attempts: {status}" - ))); - } - - warn!(?status, retry_count, "Connection failed, retrying in 3 seconds..."); - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - } - } - } - } + account: &SimpleAccount, + object_root: [u8; 32], + ) -> Result { + let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); - pub async fn generate_proxy_key( - &mut self, - module_id: ModuleId, - consensus_pubkey: BlsPublicKey, - ) -> Result, SignerModuleError> { - let Some(consensus_account) = self - .consensus_accounts() - .iter() - .find(|account| { - account.public_key.is_some_and(|account_pk| account_pk == consensus_pubkey) + let channel = self + .connections + .get(&account.server) + .ok_or(SignerModuleError::DirkCommunicationError("Unknown Dirk host".to_string()))?; + + let response = SignerClient::new(channel.clone()) + .sign(SignRequest { + data: object_root.to_vec(), + domain: domain.to_vec(), + id: Some(crate::proto::v1::sign_request::Id::PublicKey( + account.public_key.to_vec(), + )), }) - .map(|account| account.complete_name()) - else { - return Err(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec())); - }; - - let consensus_account_info = self - .accounts() - .into_iter() - .find(|account| account.complete_name() == consensus_account) - .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; - - let uuid = uuid::Uuid::new_v4(); - let account_name = format!("{consensus_account}/{module_id}/{uuid}"); - let new_password = Self::random_password(); - - match consensus_account_info.wallet_type { - WalletType::Simple => { - trace!(account = account_name, "Sending AccountManager/Generate request to Dirk"); - let channel = self.get_channel_for_pubkey(&consensus_pubkey)?; - let proxy_key = make_generate_proxy_request( - GenerateRequest { - account: account_name.clone(), - passphrase: new_password.as_bytes().to_vec(), - participants: 1, - signing_threshold: 1, - }, - channel.clone(), - ) - .await?; - - // Store the password for future use - self.store_password(account_name.clone(), new_password.clone())?; - - // Get the consensus account info to copy host information - let consensus_account = - self.accounts.get(&hex::encode(consensus_pubkey)).ok_or_else(|| { - SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()) - })?; - - let first_host = consensus_account.hosts.first().ok_or_else(|| { - SignerModuleError::Internal( - "Consensus account has no associated hosts".to_string(), - ) - })?; - - // Remove the wallet part from the name - let account_name_without_wallet = - String::from(account_name.split_once("/").map(|(_, n)| n).unwrap_or_default()); - - let delegation = self - .insert_proxy_account(proxy_key, consensus_pubkey, module_id, Account { - wallet: consensus_account.wallet.clone(), - name: account_name_without_wallet.clone(), - public_key: Some(proxy_key), - hosts: vec![HostInfo { - url: first_host.url.clone(), - participant_id: first_host.participant_id, - }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy: true, - }) - .await?; - - // Unlock the account for immediate use - self.unlock_account(account_name, new_password).await?; - - Ok(delegation) - } - WalletType::Distributed => { - // Pick the first available host to generate the key, Dirk will handle the - // peers. - let host = consensus_account_info.hosts.first().ok_or_else(|| { - SignerModuleError::Internal( - "No hosts available for consensus account".to_string(), - ) - })?; - let channel = self.channels.get(&host.url).cloned().ok_or_else(|| { - SignerModuleError::Internal(format!("No channel found for host {}", host.url)) - })?; - - trace!(host = host.url, "Sending generate request for distributed proxy key"); - let proxy_key = make_generate_proxy_request( - GenerateRequest { - account: account_name.clone(), - passphrase: new_password.as_bytes().to_vec(), - participants: consensus_account_info.hosts.len() as u32, - signing_threshold: consensus_account_info.signing_threshold, - }, - channel.clone(), - ) - .await?; - // Store the password for future use - self.store_password(account_name.clone(), new_password.clone())?; - - let consensus_name = consensus_account_info.name; - let delegation = self - .insert_proxy_account(proxy_key, consensus_pubkey, module_id.clone(), Account { - wallet: consensus_account_info.wallet.clone(), - name: format!("{consensus_name}/{module_id}/{uuid}"), - public_key: Some(proxy_key), - hosts: consensus_account_info.hosts.clone(), - wallet_type: WalletType::Distributed, - signing_threshold: consensus_account_info.signing_threshold, - is_proxy: true, - }) - .await?; - - Ok(delegation) - } - } - } - - async fn insert_proxy_account( - &mut self, - proxy_key: BlsPublicKey, - delegator: BlsPublicKey, - module_id: ModuleId, - account: Account, - ) -> Result, SignerModuleError> { - let hashmap_key = hex::encode(proxy_key); - self.accounts.insert(hashmap_key.clone(), account); - let added = self.accounts.get(&hashmap_key).ok_or(SignerModuleError::Internal( - "Failed to add new proxy account to accounts map".to_string(), - ))?; - trace!(?hashmap_key, ?added, "Proxy account added"); - - // Get delegation signature from the consensus account - let message = ProxyDelegation { delegator, proxy: proxy_key }; - let signature = self.request_signature(delegator, message.tree_hash_root().0, true).await?; - let delegation: SignedProxyDelegation = - SignedProxyDelegation { message, signature }; - - if let Some(store) = &self.proxy_store { - store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { - SignerModuleError::Internal(format!("error storing delegation signature: {err}")) + .await + .map_err(|_| { + SignerModuleError::DirkCommunicationError("Failed to sign object".to_string()) })?; + + if response.get_ref().state() != ResponseState::Succeeded { + return Err(SignerModuleError::DirkCommunicationError( + "Failed to sign object".to_string(), + )); } - Ok(delegation) + BlsSignature::try_from(response.into_inner().signature.as_slice()).map_err(|_| { + SignerModuleError::DirkCommunicationError("Failed to parse signature".to_string()) + }) } - pub async fn request_signature( + // TODO: Improve await times + async fn request_distributed_signature( &self, - pubkey: BlsPublicKey, + account: &DistributedAccount, object_root: [u8; 32], - is_consensus: bool, ) -> Result { - let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); - let account = if is_consensus { - self.get_consensus_account(pubkey) - .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))? - .clone() - } else { - self.get_proxy_account(pubkey) - .ok_or_else(|| SignerModuleError::UnknownProxySigner(pubkey.to_vec()))? - .clone() - }; + let mut partials = Vec::with_capacity(account.participants.len()); + + for (id, endpoint) in account.participants.iter() { + let Some(channel) = self.connections.get(endpoint) else { + warn!("Couldn't find server {endpoint}"); + continue; + }; - match account.wallet_type { - WalletType::Simple => { - let channel = self.get_channel_for_pubkey(&pubkey)?; - self.sign_with_channel(channel, pubkey, &account, domain, object_root).await + let Ok(response) = SignerClient::new(channel.clone()) + .sign(SignRequest { + data: object_root.to_vec(), + domain: compute_domain(self.chain, COMMIT_BOOST_DOMAIN).to_vec(), + id: Some(crate::proto::v1::sign_request::Id::Account(format!( + "{}/{}", + account.wallet, account.name + ))), + }) + .await + else { + warn!("Failed to sign object with server {endpoint}"); + continue; + }; + + if response.get_ref().state() != ResponseState::Succeeded { + warn!("Failed to sign object with server {endpoint}"); + continue; } - WalletType::Distributed => { - let num_hosts_needed = account.signing_threshold as usize; - - if account.hosts.len() < num_hosts_needed { - return Err(SignerModuleError::Internal(format!( - "Not enough hosts available. Need {} but only have {}", - num_hosts_needed, - account.hosts.len() - ))); - } - let mut set = JoinSet::new(); + let Ok(signature) = BlsSignature::try_from(response.into_inner().signature.as_slice()) + else { + warn!("Failed to parse signature from server {endpoint}"); + continue; + }; - // Spawn tasks for each host - for host in &account.hosts { - let Some(channel) = self.channels.get(&host.url).cloned() else { - continue; - }; - let dirk = self.clone(); - let account = account.clone(); - let server_url = host.url.clone(); - let participant_id = host.participant_id; + partials.push((signature, *id)); + } - set.spawn(async move { - trace!(host = server_url, "Requesting signature shard for creating proxy"); + if partials.len() < account.threshold as usize { + return Err(SignerModuleError::DirkCommunicationError( + "Failed to get enough partial signatures".to_string(), + )); + } - match dirk - .sign_with_channel(channel, pubkey, &account, domain, object_root) - .await - { - Ok(signature) => { - trace!( - host = server_url, - participant_id = participant_id, - "Got signature shard" - ); - Ok((signature, participant_id)) - } - Err(e) => { - warn!("Failed to get signature from host {}: {}", server_url, e); - Err(e) - } - } - }); - } + aggregate_partial_signatures(partials.as_slice()) + .map_err(|e| SignerModuleError::Internal(e.to_string())) + } - let mut signatures = Vec::new(); - let mut identifiers = Vec::new(); - - // Collect results until we have enough signatures - while let Some(result) = set.join_next().await { - // Check if we already have enough signatures before processing more - if signatures.len() >= num_hosts_needed { - trace!( - "Already have enough signatures ({}/{}), cancelling remaining tasks", - signatures.len(), - num_hosts_needed - ); - set.abort_all(); - break; - } + pub async fn generate_proxy_key( + &mut self, + module: &ModuleId, + consensus: BlsPublicKey, + ) -> Result, SignerModuleError> { + let proxy_account = match self.consensus_accounts.get(&consensus) { + Some(Account::Simple(account)) => { + self.generate_simple_proxy_account(account, module).await? + } + Some(Account::Distributed(account)) => { + self.generate_distributed_proxy_key(account, module).await? + } + None => return Err(SignerModuleError::UnknownConsensusSigner(consensus.to_vec())), + }; - if let Ok(Ok((signature, id))) = result { - signatures.push(signature); - identifiers.push(id); - trace!("Got signature {}/{}", signatures.len(), num_hosts_needed); + self.proxy_accounts.insert(proxy_account.inner.public_key(), proxy_account.clone()); - if signatures.len() >= num_hosts_needed { - trace!("Already have enough signatures ({}/{}), cancelling remaining tasks", - signatures.len(), num_hosts_needed); - set.abort_all(); - break; - } - } - } + Ok(SignedProxyDelegation { + message: ProxyDelegation { + delegator: consensus, + proxy: proxy_account.inner.public_key(), + }, + // TODO: Sign + signature: Default::default(), + }) + } - if signatures.len() < num_hosts_needed { - return Err(SignerModuleError::Internal(format!( - "Could not collect enough signatures. Need {} but only got {}", - num_hosts_needed, - signatures.len() - ))); - } + async fn generate_simple_proxy_account( + &self, + consensus: &SimpleAccount, + module: &ModuleId, + ) -> Result { + let channel = self + .connections + .get(&consensus.server) + .ok_or(SignerModuleError::DirkCommunicationError("Unknown Dirk host".to_string()))?; - trace!( - num_shards = signatures.len(), - ?identifiers, - "Recovering master signature from shards" - ); + let uuid = uuid::Uuid::new_v4(); + let response = AccountManagerClient::new(channel.clone()) + .generate(GenerateRequest { + account: format!("{}/{}/{module}/{uuid}", consensus.wallet, consensus.name), + passphrase: Default::default(), + participants: 1, + signing_threshold: 1, + }) + .await + .map_err(|e| SignerModuleError::DirkCommunicationError(e.to_string()))?; - aggregate_partial_signatures(&signatures, &identifiers).ok_or_else(|| { - SignerModuleError::Internal( - "Failed to recover master signature from shards".to_string(), - ) - }) - } + if response.get_ref().state() != ResponseState::Succeeded { + return Err(SignerModuleError::DirkCommunicationError(format!( + "Failed to generate proxy key: {}", + response.get_ref().message + ))); } - } - // Helper method to sign with a specific channel - async fn sign_with_channel( - &self, - channel: Channel, - pubkey: BlsPublicKey, - account: &Account, - domain: [u8; 32], - object_root: [u8; 32], - ) -> Result { - let id = match account.wallet_type { - WalletType::Simple => SignerId::PublicKey(pubkey.to_vec()), - WalletType::Distributed => SignerId::Account(account.complete_name()), + let proxy_key = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) + .map_err(|_| { + SignerModuleError::DirkCommunicationError("Failed to parse proxy key".to_string()) + })?; + + let proxy_account = ProxyAccount { + consensus: Account::Simple(consensus.clone()), + module: module.clone(), + inner: Account::Simple(SimpleAccount { + public_key: proxy_key, + server: consensus.server.clone(), + wallet: consensus.wallet.clone(), + name: format!("{}/{module}/{uuid}", consensus.name), + }), }; - trace!( - %pubkey, - ?id, - is_proxy = account.is_proxy, - wallet_type = ?account.wallet_type, - "Sending Signer/Sign request to Dirk" - ); + Ok(proxy_account) + } - let mut signer_client = SignerClient::new(channel.clone()); - let sign_request = tonic::Request::new(SignRequest { - id: Some(id.clone()), - domain: domain.to_vec(), - data: object_root.to_vec(), - }); + async fn generate_distributed_proxy_key( + &self, + consensus: &DistributedAccount, + module: &ModuleId, + ) -> Result { + let uuid = uuid::Uuid::new_v4(); - let sign_response = signer_client - .sign(sign_request) + // TODO: Improve this + let channel = self.connections.get(consensus.participants.get(&1).unwrap()).unwrap(); + let response = AccountManagerClient::new(channel.clone()) + .generate(GenerateRequest { + account: format!("{}/{}/{module}/{uuid}", consensus.wallet, consensus.name), + passphrase: Default::default(), + participants: consensus.participants.len() as u32, + signing_threshold: consensus.threshold, + }) .await - .map_err(|err| DirkCommunicationError(format!("error on sign request: {err}")))?; - - // Retry if unlock config is set - let sign_response = match sign_response.get_ref().state() { - ResponseState::Denied if self.unlock => { - info!("Failed to sign message, account {pubkey:#} may be locked. Unlocking and retrying."); - - let account_name = account.complete_name(); - self.unlock_account( - account_name.clone(), - self.read_password(account_name.clone())?, - ) - .await?; - - let sign_request = tonic::Request::new(SignRequest { - id: Some(id), - domain: domain.to_vec(), - data: object_root.to_vec(), - }); - signer_client.sign(sign_request).await.map_err(|err| { - DirkCommunicationError(format!("error on sign request: {err}")) - })? - } - _ => sign_response, - }; + .map_err(|e| SignerModuleError::DirkCommunicationError(e.to_string()))?; - if sign_response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError("sign request returned error".to_string())); + if response.get_ref().state() != ResponseState::Succeeded { + return Err(SignerModuleError::DirkCommunicationError(format!( + "Failed to generate proxy key: {}", + response.get_ref().message + ))); } - Ok(BlsSignature::from( - FixedBytes::try_from(sign_response.into_inner().signature.as_slice()).map_err( - |_| DirkCommunicationError("return value is not a valid signature".to_string()), - )?, - )) + let proxy_key = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) + .map_err(|_| { + SignerModuleError::DirkCommunicationError("Failed to parse proxy key".to_string()) + })?; + + let proxy_account = ProxyAccount { + consensus: Account::Distributed(consensus.clone()), + module: module.clone(), + inner: Account::Distributed(DistributedAccount { + composite_public_key: proxy_key, + participants: consensus.participants.clone(), + threshold: consensus.threshold, + wallet: consensus.wallet.clone(), + name: format!("{}/{module}/{uuid}", consensus.name), + }), + }; + + Ok(proxy_account) } } -/// Get the accounts for the wallets passed as argument -async fn get_accounts_in_wallets( - channel: Channel, - wallets: Vec, -) -> Result<(Vec, Vec), SignerModuleError> { - trace!(?wallets, "Sending Lister/ListAccounts request to Dirk"); - - let mut client = ListerClient::new(channel); - let pubkeys_request = tonic::Request::new(ListAccountsRequest { paths: wallets }); - let pubkeys_response = client - .list_accounts(pubkeys_request) - .await - .map_err(|err| DirkCommunicationError(format!("error listing accounts: {err}")))?; - - if pubkeys_response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError("list accounts request returned error".to_string())); +async fn connect(server: &DirkHostConfig, certs: &CertConfig) -> eyre::Result { + let mut tls_config = ClientTlsConfig::new().identity(certs.client.clone()); + if let Some(ca) = &certs.ca { + tls_config = tls_config.ca_certificate(ca.clone()); + } + if let Some(server_name) = &server.server_name { + tls_config = tls_config.domain_name(server_name); } - let inner = pubkeys_response.into_inner(); - Ok((inner.accounts, inner.distributed_accounts)) + Channel::from_shared(server.url.to_string()) + .map_err(eyre::Error::from)? + .tls_config(tls_config) + .map_err(eyre::Error::from)? + .connect() + .await + .map_err(eyre::Error::from) } -pub fn aggregate_partial_signatures( - partials: &[BlsSignature], - identifiers: &[u64], -) -> Option { - trace!("Aggregating partial signatures"); - // Ensure the number of partial signatures matches the number of identifiers - if partials.len() != identifiers.len() { - warn!("aggregate_partial_signatures: Invalid number of partial signatures"); - return None; - } +fn decompose_name(full_name: &str) -> eyre::Result<(&str, &str)> { + full_name.split_once('/').ok_or_else(|| eyre::eyre!("Invalid account name")) +} +pub fn aggregate_partial_signatures( + partials: &[(BlsSignature, u32)], +) -> eyre::Result { // Deserialize partial signatures into G2 points - let mut points = Vec::new(); - for sig in partials { + let mut shares: HashMap = HashMap::new(); + for (sig, id) in partials { if sig.len() != BLS_SIGNATURE_BYTES_LEN { - warn!("aggregate_partial_signatures: Invalid signature length"); - return None; + bail!("Invalid signature length") } let arr: [u8; BLS_SIGNATURE_BYTES_LEN] = (*sig).into(); let opt: Option = G2Affine::from_compressed(&arr).into(); let opt: Option = G2Projective::from(&opt.unwrap()).into(); if let Some(point) = opt { - points.push(point); + shares.insert(*id, point); } else { - warn!("aggregate_partial_signatures: Failed to deserialize signature"); - return None; + bail!("Failed to deserialize signature") } } - // Create a map of identifiers to their corresponding points - let mut shares: HashMap = HashMap::new(); - for (id, point) in identifiers.iter().zip(points.iter()) { - shares.insert(*id, point); - } - // Perform Lagrange interpolation to recover the master signature let mut recovered = G2Projective::identity(); for (id, point) in &shares { @@ -879,37 +522,10 @@ pub fn aggregate_partial_signatures( let lagrange_coeff = numerator * denominator.invert().unwrap(); // Multiply the point by the Lagrange coefficient and add to the recovered point - recovered += **point * lagrange_coeff; + recovered += *point * lagrange_coeff; } // Serialize the recovered point back into a BlsSignature let bytes = recovered.to_compressed(); - Some(bytes.into()) -} - -async fn make_generate_proxy_request( - request: GenerateRequest, - channel: Channel, -) -> Result { - let mut client = AccountManagerClient::new(channel.clone()); - let response = client - .generate(request) - .await - .map_err(|err| DirkCommunicationError(format!("error on generate request: {err}")))?; - if response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError("generate request returned error".to_string())); - } - trace!(?response, "Generated new proxy key"); - let proxy_key = - BlsPublicKey::try_from(response.into_inner().public_key.as_slice()).map_err(|_| { - DirkCommunicationError("return value is not a valid public key".to_string()) - })?; - Ok(proxy_key) -} - -/// Checks if a key name follows the proxy pattern -/// // -fn is_proxy_key_name(key_name: &str) -> bool { - key_name.split('/').count() == 3 && - uuid::Uuid::parse_str(key_name.split('/').last().unwrap_or_default()).is_ok() + Ok(bytes.into()) } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index be68db21..3a2ec17c 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -180,12 +180,12 @@ async fn handle_request_signature( }, SigningManager::Dirk(dirk_manager) => match request { SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => dirk_manager - .request_signature(pubkey, object_root, true) + .request_consensus_signature(&pubkey, object_root) .await .map(|sig| Json(sig).into_response()), SignRequest::ProxyBls(SignProxyRequest { object_root, pubkey: bls_key }) => { dirk_manager - .request_signature(bls_key, object_root, false) + .request_proxy_signature(&bls_key, object_root) .await .map(|sig| Json(sig).into_response()) } @@ -231,7 +231,7 @@ async fn handle_generate_proxy( }, SigningManager::Dirk(dirk_manager) => match request.scheme { EncryptionScheme::Bls => dirk_manager - .generate_proxy_key(module_id.clone(), request.consensus_pubkey) + .generate_proxy_key(&module_id, request.consensus_pubkey) .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), EncryptionScheme::Ecdsa => { From 656384b5f5c016ded51b756c809ec75ab46198f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Feb 2025 19:12:40 +0000 Subject: [PATCH 078/104] Use real participant IDs --- crates/common/src/config/signer.rs | 6 ++++++ crates/signer/src/manager/dirk.rs | 23 ++++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 5396c76c..599b9d14 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -43,6 +43,12 @@ pub struct DirkHostConfig { pub accounts: Vec, } +impl DirkHostConfig { + pub fn name(&self) -> Option { + self.server_name.clone().or_else(|| self.url.host_str().map(String::from)) + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub enum SignerType { diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index a0f3b101..10d3a05a 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -20,7 +20,7 @@ use crate::{ error::SignerModuleError, proto::v1::{ account_manager_client::AccountManagerClient, lister_client::ListerClient, - signer_client::SignerClient, GenerateRequest, ListAccountsRequest, ResponseState, + signer_client::SignerClient, Endpoint, GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, }, }; @@ -87,6 +87,11 @@ impl DirkManager { let mut consensus_accounts = HashMap::new(); for host in config.hosts { + let Some(host_name) = host.name() else { + warn!("Host name not found for server {}", host.url); + continue; + }; + let channel = match connect(&host, &certs).await { Ok(channel) => channel, Err(e) => { @@ -97,6 +102,7 @@ impl DirkManager { connections.insert(host.url.clone(), channel.clone()); + // TODO: Improve to minimize requests for account_name in host.accounts { let Ok((wallet, name)) = decompose_name(&account_name) else { warn!("Invalid account name {account_name}"); @@ -147,9 +153,20 @@ impl DirkManager { continue; }; + let Some(&Endpoint { id: participant_id, .. }) = account + .participants + .iter() + .find(|participant| participant.name == host_name) + else { + warn!( + "Host {host_name} not found as participant for account {account_name}" + ); + continue; + }; + match consensus_accounts.get_mut(&public_key) { Some(Account::Distributed(DistributedAccount { participants, .. })) => { - participants.insert(participants.len() as u32 + 1, host.url.clone()); + participants.insert(participant_id as u32, host.url.clone()); } Some(Account::Simple(_)) => { bail!("Distributed public key already exists for simple account"); @@ -157,7 +174,7 @@ impl DirkManager { None => { let mut participants = HashMap::with_capacity(account.participants.len()); - participants.insert(1, host.url.clone()); + participants.insert(participant_id as u32, host.url.clone()); consensus_accounts.insert( public_key, Account::Distributed(DistributedAccount { From 514f4b83e5bc6ab605c593019bcd99359bd93a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Feb 2025 19:29:41 +0000 Subject: [PATCH 079/104] Simplify requests for logs --- crates/signer/src/manager/dirk.rs | 10 ++++------ crates/signer/src/manager/local.rs | 4 ++-- crates/signer/src/manager/mod.rs | 17 +++++++---------- crates/signer/src/service.rs | 5 ++--- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 10d3a05a..f5f0ea98 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -207,16 +207,14 @@ impl DirkManager { Ok(self) } - pub async fn consensus_pubkeys(&self) -> Vec { - self.consensus_accounts.keys().cloned().collect() + pub fn available_consensus_signers(&self) -> usize { + self.consensus_accounts.len() } - // TODO - pub async fn proxies(&self) -> Vec<()> { - Vec::new() + pub fn available_proxy_signers(&self) -> usize { + self.proxy_accounts.len() } - // TODO pub fn get_consensus_proxy_maps(&self, module: &ModuleId) -> Vec { self.consensus_accounts .iter() diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index e321ed92..358ef912 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -257,8 +257,8 @@ impl LocalSigningManager { Ok(keys) } - pub fn proxies(&self) -> &ProxySigners { - &self.proxy_signers + pub fn available_proxy_signers(&self) -> usize { + self.proxy_signers.bls_signers.len() + self.proxy_signers.ecdsa_signers.len() } } diff --git a/crates/signer/src/manager/mod.rs b/crates/signer/src/manager/mod.rs index 62547cb1..2e608674 100644 --- a/crates/signer/src/manager/mod.rs +++ b/crates/signer/src/manager/mod.rs @@ -15,25 +15,22 @@ pub enum SigningManager { impl SigningManager { /// Amount of consensus signers available - pub async fn available_consensus_signers(&self) -> eyre::Result { + pub fn available_consensus_signers(&self) -> usize { match self { - SigningManager::Local(local_manager) => Ok(local_manager.consensus_pubkeys().len()), - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.consensus_pubkeys().await.len()), + SigningManager::Local(local_manager) => local_manager.consensus_pubkeys().len(), + SigningManager::Dirk(dirk_manager) => dirk_manager.available_consensus_signers(), } } /// Amount of proxy signers available - pub async fn available_proxy_signers(&self) -> eyre::Result { + pub fn available_proxy_signers(&self) -> usize { match self { - SigningManager::Local(local_manager) => { - let proxies = local_manager.proxies(); - Ok(proxies.bls_signers.len() + proxies.ecdsa_signers.len()) - } - SigningManager::Dirk(dirk_manager) => Ok(dirk_manager.proxies().await.len()), + SigningManager::Local(local_manager) => local_manager.available_proxy_signers(), + SigningManager::Dirk(dirk_manager) => dirk_manager.available_proxy_signers(), } } - pub async fn get_consensus_proxy_maps( + pub fn get_consensus_proxy_maps( &self, module_id: &ModuleId, ) -> Result, SignerModuleError> { diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 3a2ec17c..de371441 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -64,8 +64,8 @@ impl SigningService { jwts: config.jwts.into(), }; - let loaded_consensus = state.manager.read().await.available_consensus_signers().await?; - let loaded_proxies = state.manager.read().await.available_proxy_signers().await?; + let loaded_consensus = state.manager.read().await.available_consensus_signers(); + let loaded_proxies = state.manager.read().await.available_proxy_signers(); info!(version = COMMIT_BOOST_VERSION, commit = COMMIT_BOOST_COMMIT, modules =? module_ids, port =? config.server_port, loaded_consensus, loaded_proxies, "Starting signing service"); @@ -140,7 +140,6 @@ async fn handle_get_pubkeys( .read() .await .get_consensus_proxy_maps(&module_id) - .await .map_err(|err| SignerModuleError::Internal(err.to_string()))?; let res = GetPubkeysResponse { keys }; From ae0612b01d5edcd7dbc23834d1f71e1527dd4d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Feb 2025 19:39:46 +0000 Subject: [PATCH 080/104] Sign proxy key delegation --- crates/signer/src/manager/dirk.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index f5f0ea98..e713b33a 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -5,7 +5,9 @@ use alloy::{ }; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ - commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, + commit::request::{ + ConsensusProxyMap, ProxyDelegation, ProxyDelegationBls, SignedProxyDelegation, + }, config::{DirkConfig, DirkHostConfig}, constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, @@ -15,6 +17,7 @@ use cb_common::{ use eyre::bail; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use tracing::{debug, error, info, warn}; +use tree_hash::TreeHash; use crate::{ error::SignerModuleError, @@ -375,6 +378,11 @@ impl DirkManager { None => return Err(SignerModuleError::UnknownConsensusSigner(consensus.to_vec())), }; + let message = + ProxyDelegationBls { delegator: consensus, proxy: proxy_account.inner.public_key() }; + let delegation_signature = + self.request_consensus_signature(&consensus, message.tree_hash_root().0).await?; + self.proxy_accounts.insert(proxy_account.inner.public_key(), proxy_account.clone()); Ok(SignedProxyDelegation { @@ -382,8 +390,7 @@ impl DirkManager { delegator: consensus, proxy: proxy_account.inner.public_key(), }, - // TODO: Sign - signature: Default::default(), + signature: delegation_signature, }) } From ad04d69498bdba1467f29af24a03abfcbcb3b214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Feb 2025 19:46:23 +0000 Subject: [PATCH 081/104] Small code improve --- crates/signer/src/manager/dirk.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index e713b33a..135d321f 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -379,19 +379,13 @@ impl DirkManager { }; let message = - ProxyDelegationBls { delegator: consensus, proxy: proxy_account.inner.public_key() }; + ProxyDelegation { delegator: consensus, proxy: proxy_account.inner.public_key() }; let delegation_signature = self.request_consensus_signature(&consensus, message.tree_hash_root().0).await?; self.proxy_accounts.insert(proxy_account.inner.public_key(), proxy_account.clone()); - Ok(SignedProxyDelegation { - message: ProxyDelegation { - delegator: consensus, - proxy: proxy_account.inner.public_key(), - }, - signature: delegation_signature, - }) + Ok(SignedProxyDelegation { message, signature: delegation_signature }) } async fn generate_simple_proxy_account( From 4222698f88e1566a83cdec67081c6b371c8477de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Feb 2025 20:09:39 +0000 Subject: [PATCH 082/104] Add delegation signature store --- crates/common/src/config/signer.rs | 3 ++- crates/signer/src/manager/dirk.rs | 31 +++++++++++++++++++++++++----- crates/signer/src/service.rs | 2 +- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 599b9d14..5866b885 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -79,7 +79,8 @@ pub enum SignerType { /// Whether to unlock the accounts in case they are locked #[serde(default)] unlock: bool, - /// How to store proxy keys + /// How to store proxy key delegations + /// ERC2335 is not supported with Dirk signer store: Option, }, } diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 135d321f..2d524f10 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -15,6 +15,7 @@ use cb_common::{ types::{Chain, ModuleId}, }; use eyre::bail; +use rand::Rng; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; @@ -80,10 +81,11 @@ pub struct DirkManager { connections: HashMap, consensus_accounts: HashMap, proxy_accounts: HashMap, + delegations_store: Option, } impl DirkManager { - pub async fn new_from_config(chain: Chain, config: DirkConfig) -> eyre::Result { + pub async fn new(chain: Chain, config: DirkConfig) -> eyre::Result { let certs = CertConfig { ca: config.cert_auth, client: config.client_cert }; let mut connections = HashMap::with_capacity(config.hosts.len()); @@ -202,12 +204,22 @@ impl DirkManager { consensus_accounts.keys().map(|k| k.to_string()).collect::>().join(", ") ); - Ok(Self { chain, certs, connections, consensus_accounts, proxy_accounts: HashMap::new() }) + Ok(Self { + chain, + certs, + connections, + consensus_accounts, + proxy_accounts: HashMap::new(), + delegations_store: None, + }) } - // TODO pub fn with_proxy_store(self, store: ProxyStore) -> eyre::Result { - Ok(self) + if let ProxyStore::ERC2335 { .. } = store { + return Err(eyre::eyre!("ERC2335 proxy store not supported")); + } + + Ok(Self { delegations_store: Some(store), ..self }) } pub fn available_consensus_signers(&self) -> usize { @@ -383,9 +395,18 @@ impl DirkManager { let delegation_signature = self.request_consensus_signature(&consensus, message.tree_hash_root().0).await?; + let delegation = SignedProxyDelegation { message, signature: delegation_signature }; + + if let Some(store) = &self.delegations_store { + store.store_proxy_bls_delegation(module, &delegation).map_err(|e| { + warn!("Couldn't store delegation signature: {e}"); + SignerModuleError::Internal("Couldn't store delegation signature".to_string()) + })?; + } + self.proxy_accounts.insert(proxy_account.inner.public_key(), proxy_account.clone()); - Ok(SignedProxyDelegation { message, signature: delegation_signature }) + Ok(delegation) } async fn generate_simple_proxy_account( diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index de371441..90a2bb3b 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -285,7 +285,7 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result { - let mut manager = DirkManager::new_from_config(config.chain, dirk).await?; + let mut manager = DirkManager::new(config.chain, dirk).await?; if let Some(store) = config.store { manager = manager.with_proxy_store(store.init_from_env()?)?; } From 22da695a8f02facba03f00c837ab3dddac71dc3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Feb 2025 20:17:49 +0000 Subject: [PATCH 083/104] Store password --- crates/signer/src/manager/dirk.rs | 43 +- crates/signer/src/manager/dirk2.rs | 1021 ++++++++++++++++++++++++++++ 2 files changed, 1061 insertions(+), 3 deletions(-) create mode 100644 crates/signer/src/manager/dirk2.rs diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 2d524f10..38a636a4 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, io::Write, path::PathBuf}; use alloy::{ hex, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN, transports::http::reqwest::Url, @@ -41,6 +41,15 @@ enum Account { Distributed(DistributedAccount), } +impl Account { + pub fn full_name(&self) -> String { + match self { + Account::Simple(account) => format!("{}/{}", account.wallet, account.name), + Account::Distributed(account) => format!("{}/{}", account.wallet, account.name), + } + } +} + #[derive(Clone, Debug)] struct SimpleAccount { public_key: BlsPublicKey, @@ -81,6 +90,7 @@ pub struct DirkManager { connections: HashMap, consensus_accounts: HashMap, proxy_accounts: HashMap, + secrets_path: PathBuf, delegations_store: Option, } @@ -210,6 +220,7 @@ impl DirkManager { connections, consensus_accounts, proxy_accounts: HashMap::new(), + secrets_path: config.secrets_path, delegations_store: None, }) } @@ -420,10 +431,12 @@ impl DirkManager { .ok_or(SignerModuleError::DirkCommunicationError("Unknown Dirk host".to_string()))?; let uuid = uuid::Uuid::new_v4(); + let password = random_password(); + let response = AccountManagerClient::new(channel.clone()) .generate(GenerateRequest { account: format!("{}/{}/{module}/{uuid}", consensus.wallet, consensus.name), - passphrase: Default::default(), + passphrase: password.as_bytes().to_vec(), participants: 1, signing_threshold: 1, }) @@ -453,6 +466,11 @@ impl DirkManager { }), }; + self.store_password(&proxy_account, password).map_err(|e| { + error!("Failed to store password: {e}"); + SignerModuleError::Internal("Failed to store password".to_string()) + })?; + Ok(proxy_account) } @@ -462,13 +480,14 @@ impl DirkManager { module: &ModuleId, ) -> Result { let uuid = uuid::Uuid::new_v4(); + let password = random_password(); // TODO: Improve this let channel = self.connections.get(consensus.participants.get(&1).unwrap()).unwrap(); let response = AccountManagerClient::new(channel.clone()) .generate(GenerateRequest { account: format!("{}/{}/{module}/{uuid}", consensus.wallet, consensus.name), - passphrase: Default::default(), + passphrase: password.as_bytes().to_vec(), participants: consensus.participants.len() as u32, signing_threshold: consensus.threshold, }) @@ -499,8 +518,20 @@ impl DirkManager { }), }; + self.store_password(&proxy_account, password).map_err(|e| { + error!("Failed to store password: {e}"); + SignerModuleError::Internal("Failed to store password".to_string()) + })?; + Ok(proxy_account) } + + fn store_password(&self, account: &ProxyAccount, password: String) -> eyre::Result<()> { + let path = self.secrets_path.join(account.inner.full_name()); + let mut file = std::fs::File::create(path)?; + file.write_all(password.as_bytes())?; + Ok(()) + } } async fn connect(server: &DirkHostConfig, certs: &CertConfig) -> eyre::Result { @@ -566,3 +597,9 @@ pub fn aggregate_partial_signatures( let bytes = recovered.to_compressed(); Ok(bytes.into()) } + +/// Generate a random password of 64 hex-characters +fn random_password() -> String { + let password_bytes: [u8; 32] = rand::thread_rng().gen(); + hex::encode(password_bytes) +} diff --git a/crates/signer/src/manager/dirk2.rs b/crates/signer/src/manager/dirk2.rs new file mode 100644 index 00000000..3597e817 --- /dev/null +++ b/crates/signer/src/manager/dirk2.rs @@ -0,0 +1,1021 @@ +use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf}; + +use alloy::{ + hex, primitives::FixedBytes, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN, + transports::http::reqwest::Url, +}; +use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; +use cb_common::{ + commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, + config::DirkConfig, + constants::COMMIT_BOOST_DOMAIN, + signature::compute_domain, + signer::{BlsPublicKey, BlsSignature, ProxyStore}, + types::{Chain, ModuleId}, +}; +use rand::Rng; +use tokio::task::JoinSet; +use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; +use tracing::{info, trace, warn}; +use tree_hash::TreeHash; + +use crate::{ + error::SignerModuleError::{self, DirkCommunicationError}, + proto::v1::{ + account_manager_client::AccountManagerClient, lister_client::ListerClient, + sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, + DistributedAccount, GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, + UnlockAccountRequest, + }, +}; + +#[derive(Clone, Debug)] +enum WalletType { + Simple, + Distributed, +} + +#[derive(Clone, Debug)] +struct HostInfo { + url: String, + participant_id: u64, +} + +#[derive(Clone, Debug)] +struct Account { + wallet: String, + name: String, + public_key: Option, + hosts: Vec, + wallet_type: WalletType, + signing_threshold: u32, + is_proxy: bool, +} + +impl Account { + pub fn complete_name(&self) -> String { + format!("{}/{}", self.wallet, self.name) + } +} + +#[derive(Clone, Debug)] +struct ModuleConsensusProxyMap { + pub consensus: BlsPublicKey, + pub proxy_bls: Vec<(BlsPublicKey, ModuleId)>, +} + +struct ServerInfo { + endpoint: Url, + server_name: Option, +} + +#[derive(Clone, Debug)] +struct CertConfig { + client_cert: Identity, + ca_cert: Option, +} + +#[derive(Clone, Debug)] +pub struct DirkManager { + chain: Chain, + certs: CertConfig, + channels2: HashMap, // endpoint -> channel + channels: HashMap, // server_name -> channel + accounts: HashMap, // pubkey -> account + unlock: bool, + secrets_path: PathBuf, + proxy_store: Option, + proxy_maps: Vec, +} + +impl DirkManager { + pub async fn new_from_config(chain: Chain, config: DirkConfig) -> eyre::Result { + let mut tls_configs = Vec::with_capacity(config.hosts.len()); + + // Create a TLS config for each host + for host in config.hosts.clone() { + let mut tls_config = ClientTlsConfig::new().identity(config.client_cert.clone()); + + if let Some(ca) = &config.cert_auth { + tls_config = tls_config.ca_certificate(ca.clone()); + } + + if let Some(server_name) = host.server_name.clone() { + tls_config = tls_config.domain_name(server_name); + tls_configs.push(tls_config); + } else { + trace!("Server name for host {0} not present", host.url); + } + } + + // Create a channel for each host, attempt to connect, + // and save it into a hashmap Host->Channel + let mut channels = HashMap::with_capacity(tls_configs.len()); + for (i, tls_config) in tls_configs.iter().enumerate() { + let config_host = config.hosts[i].clone(); + let url = config_host.url.to_string(); + match Channel::from_shared(url.clone()) + .map_err(|_| eyre::eyre!("Invalid Dirk URL"))? + .tls_config(tls_config.clone()) + .map_err(|_| eyre::eyre!("Invalid Dirk TLS config"))? + .connect() + .await + { + Ok(ch) => { + channels.insert(url.clone(), ch); + } + Err(e) => { + trace!("Couldn't connect to Dirk host {}: {e}", url); + } + } + } + + let mut accounts: HashMap = HashMap::new(); + + for host in config.hosts.clone() { + let url = host.url.to_string(); + let channel = channels + .get(&url) + .ok_or(eyre::eyre!("Couldn't connect to Dirk host {url}"))? + .clone(); + + let (dirk_accounts, dirk_distributed_accounts) = get_accounts_in_wallets( + channel.clone(), + host.accounts + .iter() + .filter_map(|account| Some(account.split_once("/")?.0.to_string())) + .collect(), + ) + .await?; + + for account_name in host.accounts.clone() { + let (wallet, _) = account_name.split_once("/").ok_or(eyre::eyre!( + "Invalid account name: {account_name}. It must be in format wallet/account" + ))?; + + // Handle simple accounts + for dirk_account in dirk_accounts.iter().filter(|a| { + a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) + }) { + let public_key = BlsPublicKey::try_from(dirk_account.public_key.as_slice())?; + let key_name = dirk_account + .name + .split_once("/") + .map(|(_wallet, name)| name) + .unwrap_or_default(); + trace!(?dirk_account.name, "Adding account to hashmap"); + + let is_proxy = is_proxy_key_name(key_name); + + accounts.insert(hex::encode(public_key), Account { + wallet: wallet.to_string(), + name: key_name.to_string(), + public_key: Some(public_key), + hosts: vec![HostInfo { url: url.clone(), participant_id: 1 }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy, + }); + } + + // Handle distributed accounts + for dist_account in dirk_distributed_accounts.iter().filter(|a| { + a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) + }) { + let public_key = + BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; + let key_name = dist_account + .name + .split_once("/") + .map(|(_wallet, name)| name) + .unwrap_or_default(); + let is_proxy = is_proxy_key_name(key_name); + + trace!(?dist_account.name, "Adding distributed account to hashmap"); + + // Find the participant ID for this host from the participants list + let url_hostname = host + .url + .host_str() + .ok_or_else(|| eyre::eyre!("Invalid Dirk host {}", host.url))?; + trace!(%url_hostname, "url_hostname"); + let current_participant = + dist_account.participants.first().ok_or_else(|| { + eyre::eyre!( + "Host {} not found in distributed account participants", + url + ) + })?; + + accounts + .entry(hex::encode(public_key)) + .and_modify(|account| { + if !account.hosts.iter().any(|host| host.url == url) { + account.hosts.push(HostInfo { + url: url.clone(), + participant_id: current_participant.id, + }); + } + }) + .or_insert_with(|| Account { + wallet: wallet.to_string(), + name: key_name.to_string(), + public_key: Some(public_key), + hosts: vec![HostInfo { + url: url.clone(), + participant_id: current_participant.id, + }], + wallet_type: WalletType::Distributed, + signing_threshold: dist_account.signing_threshold, + is_proxy, + }); + } + } + } + + let mut channels2 = HashMap::new(); + let certs = CertConfig { + client_cert: config.client_cert.clone(), + ca_cert: config.cert_auth.clone(), + }; + for host in config.hosts { + let server = ServerInfo { endpoint: host.url.clone(), server_name: host.server_name }; + let channel = match connect(server, certs.clone()).await { + Ok(channel) => channel, + Err(e) => { + warn!("Couldn't connect to Dirk server {}: {e}", host.url); + continue; + } + }; + + channels2.insert(host.url.clone(), channel.clone()); + + for account in host.accounts { + let Some((wallet, name)) = account.split_once('/') else { + warn!("Invalid account name: {account}"); + continue; + }; + + let res = ListerClient::new(channel.clone()) + .list_accounts(ListAccountsRequest { paths: vec![account.clone()] }) + .await + .unwrap(); + + if let Some(distributed_account) = res.get_ref().distributed_accounts.get(0) { + let pubkey = distributed_account.composite_public_key.clone(); + let acc = Account { + wallet: wallet.to_string(), + name: name.to_string(), + public_key: Some(BlsPublicKey::try_from(pubkey.as_slice()).unwrap()), + hosts: distributed_account + .participants + .iter() + .map(|participant| HostInfo { + url: format!("{}/{}", participant.name, participant.port), + participant_id: participant.id, + }) + .collect(), + wallet_type: WalletType::Distributed, + signing_threshold: distributed_account.signing_threshold, + is_proxy: is_proxy_key_name(&account), + }; + accounts.insert(account, acc); + } else if let Some(simple_account) = res.get_ref().accounts.get(0) { + let pubkey = simple_account.public_key.clone(); + let acc = Account { + wallet: wallet.to_string(), + name: name.to_string(), + public_key: Some(BlsPublicKey::try_from(pubkey.as_slice()).unwrap()), + hosts: vec![HostInfo { url: host.url.to_string(), participant_id: 1 }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy: is_proxy_key_name(&account), + }; + accounts.insert(account, acc); + } else { + warn!("Account {account} not found in server {}", host.url); + } + } + } + + let mut manager = Self { + chain, + certs: CertConfig { client_cert: config.client_cert, ca_cert: config.cert_auth }, + channels2, + channels, + accounts, + unlock: config.unlock, + secrets_path: config.secrets_path, + proxy_store: None, + proxy_maps: vec![], + }; + manager.build_consensus_proxy_map(); + + Ok(manager) + } + + fn accounts(&self) -> Vec { + self.accounts.values().cloned().collect() + } + + fn consensus_accounts(&self) -> Vec { + self.accounts.values().filter(|a| !a.is_proxy).cloned().collect() + } + + pub fn with_proxy_store(self, proxy_store: ProxyStore) -> eyre::Result { + Ok(Self { proxy_store: Some(proxy_store), ..self }) + } + + fn get_consensus_account(&self, pubkey: BlsPublicKey) -> Option { + self.accounts.get(&hex::encode(pubkey)).filter(|acc| !acc.is_proxy).cloned() + } + + fn get_proxy_account(&self, pubkey: BlsPublicKey) -> Option { + self.accounts.get(&hex::encode(pubkey)).filter(|acc| acc.is_proxy).cloned() + } + + /// Returns the public keys of the config-registered accounts + pub async fn consensus_pubkeys(&self) -> Vec { + self.consensus_accounts() + .iter() + .filter_map(|account| account.public_key) + .collect::>() + } + + /// Returns the public keys of all the proxy accounts found in Dirk. + /// An account is considered a proxy if its name has the format + /// `consensus_account/module_id/uuid`, where `consensus_account` is the + /// name of a config-registered account. + pub async fn proxies(&self) -> Vec { + self.accounts() + .iter() + .filter(|account| account.is_proxy) + .filter_map(|account| account.public_key) + .collect::>() + } + + pub fn get_consensus_proxy_maps(&self, module_id: &ModuleId) -> Vec { + // Filter only proxy keys whose module_id match the request one + // and also remove the module_id part from the struct, since the response + // does not include it. + self.proxy_maps + .iter() + .map(|map| { + let filtered_proxy_bls: Vec = map + .proxy_bls + .iter() + .filter_map(|(pubkey, id)| if id == module_id { Some(*pubkey) } else { None }) + .collect(); + + ConsensusProxyMap { + consensus: map.consensus, + proxy_bls: filtered_proxy_bls, + proxy_ecdsa: vec![], + } + }) + .collect() + } + + /// Builds a mapping of the proxy accounts' pubkeys by consensus account, + /// for a given module. + /// An account is considered a proxy if its name has the format + /// `consensus_account/module_id/uuid`, where `consensus_account` is the + /// name of a config-registered account. + pub fn build_consensus_proxy_map(&mut self) { + let accounts = self.accounts(); + let proxy_accounts: Vec<_> = accounts.iter().filter(|account| account.is_proxy).collect(); + + for consensus_account in self.consensus_accounts() { + let Some(consensus_key) = consensus_account.public_key else { + continue; + }; + let mut proxy_bls: Vec<(BlsPublicKey, ModuleId)> = vec![]; + + let start_of_proxy_name = format!("{}/", consensus_account.complete_name()); + for proxy in &proxy_accounts { + if proxy.complete_name().starts_with(&start_of_proxy_name) && + is_proxy_key_name(&proxy.name) + { + if let Some(pubkey) = proxy.public_key { + // Extract module_id from the proxy account's complete name + let module_id = ModuleId( + proxy.complete_name().split('/').nth(2).unwrap_or_default().to_string(), + ); + proxy_bls.push((pubkey, module_id)); + } + } + } + + self.proxy_maps.push(ModuleConsensusProxyMap { consensus: consensus_key, proxy_bls }); + } + } + + /// Generate a random password of 64 hex-characters + fn random_password() -> String { + let password_bytes: [u8; 32] = rand::thread_rng().gen(); + hex::encode(password_bytes) + } + + /// Read the password for an account from a file + fn read_password(&self, account: String) -> Result { + let path = self.secrets_path.join(format!("{account}.pass")); + trace!(path = ?path, "Reading password from file"); + fs::read_to_string(self.secrets_path.join(format!("{account}.pass"))).map_err(|err| { + SignerModuleError::Internal(format!( + "error reading password for account '{account}': {err}" + )) + }) + } + + /// Store the password for an account in a file + fn store_password(&self, account: String, password: String) -> Result<(), SignerModuleError> { + let account_dir = self + .secrets_path + .join( + account + .rsplit_once("/") + .ok_or(SignerModuleError::Internal(format!( + "account name '{account}' is invalid" + )))? + .0, + ) + .to_string_lossy() + .to_string(); + trace!(%account_dir, "Storing password in file"); + + fs::create_dir_all(account_dir.clone()).map_err(|err| { + SignerModuleError::Internal(format!("error creating dir '{account_dir}': {err}")) + })?; + fs::write(self.secrets_path.join(format!("{account}.pass")), password).map_err(|err| { + SignerModuleError::Internal(format!( + "error writing password for account '{account}': {err}" + )) + }) + } + + /// Get the associated channel for a public key + fn get_channel_for_pubkey(&self, pubkey: &BlsPublicKey) -> Result { + trace!(%pubkey, "Getting channel for public key"); + let key = hex::encode(pubkey); + let account = self + .accounts + .get(&key) + .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; + + // Try to find any available host's channel + for host in &account.hosts { + if let Some(channel) = self.channels.get(&host.url) { + return Ok(channel.clone()); + } + } + + Err(SignerModuleError::Internal("No available channel found for any host".to_string())) + } + + /// Unlock an account. For distributed accounts this is done for all it's + /// hosts. + async fn unlock_account( + &self, + account: String, + password: String, + ) -> Result<(), SignerModuleError> { + let account_entry = + self.accounts.values().find(|a| a.complete_name() == account).ok_or_else(|| { + SignerModuleError::Internal(format!("Account not found: {}", account)) + })?; + + match account_entry.wallet_type { + WalletType::Distributed => { + // For distributed accounts, unlock on all hosts + for host in &account_entry.hosts { + if let Some(channel) = self.channels.get(&host.url) { + self.unlock_account_on_channel(channel, &account, &password).await?; + } + } + } + WalletType::Simple => { + // For simple accounts, unlock on a single host + let channel = + self.get_channel_for_pubkey(account_entry.public_key.as_ref().ok_or_else( + || SignerModuleError::Internal("Account has no public key".to_string()), + )?)?; + self.unlock_account_on_channel(&channel, &account, &password).await?; + } + } + Ok(()) + } + + /// Unlock an account on a specific channel + async fn unlock_account_on_channel( + &self, + channel: &Channel, + account: &str, + password: &str, + ) -> Result<(), SignerModuleError> { + trace!(account, "unlock_account_on_channel"); + const MAX_RETRIES: u32 = 5; + let mut retry_count = 0; + + loop { + let mut client = AccountManagerClient::new(channel.clone()); + let unlock_request = tonic::Request::new(UnlockAccountRequest { + account: account.to_string(), + passphrase: password.as_bytes().to_vec(), + }); + + match client.unlock(unlock_request).await { + Ok(unlock_response) => { + if unlock_response.get_ref().state() == ResponseState::Succeeded { + return Ok(()); + } + // We have connected but an error has been returned + let err = unlock_response.get_ref(); + warn!(?err, "unlock_account_on_channel error response"); + return Err(DirkCommunicationError( + "unlock_account_on_channel error response received".to_string(), + )); + } + Err(status) => { + retry_count += 1; + if retry_count >= MAX_RETRIES { + return Err(DirkCommunicationError(format!( + "Failed to connect after {MAX_RETRIES} attempts: {status}" + ))); + } + + warn!(?status, retry_count, "Connection failed, retrying in 3 seconds..."); + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + } + } + } + } + + pub async fn generate_proxy_key( + &mut self, + module_id: ModuleId, + consensus_pubkey: BlsPublicKey, + ) -> Result, SignerModuleError> { + let Some(consensus_account) = self + .consensus_accounts() + .iter() + .find(|account| { + account.public_key.is_some_and(|account_pk| account_pk == consensus_pubkey) + }) + .map(|account| account.complete_name()) + else { + return Err(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec())); + }; + + let consensus_account_info = self + .accounts() + .into_iter() + .find(|account| account.complete_name() == consensus_account) + .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; + + let uuid = uuid::Uuid::new_v4(); + let account_name = format!("{consensus_account}/{module_id}/{uuid}"); + let new_password = Self::random_password(); + + match consensus_account_info.wallet_type { + WalletType::Simple => { + trace!(account = account_name, "Sending AccountManager/Generate request to Dirk"); + let channel = self.get_channel_for_pubkey(&consensus_pubkey)?; + let proxy_key = make_generate_proxy_request( + GenerateRequest { + account: account_name.clone(), + passphrase: new_password.as_bytes().to_vec(), + participants: 1, + signing_threshold: 1, + }, + channel.clone(), + ) + .await?; + + // Store the password for future use + self.store_password(account_name.clone(), new_password.clone())?; + + // Get the consensus account info to copy host information + let consensus_account = + self.accounts.get(&hex::encode(consensus_pubkey)).ok_or_else(|| { + SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()) + })?; + + let first_host = consensus_account.hosts.first().ok_or_else(|| { + SignerModuleError::Internal( + "Consensus account has no associated hosts".to_string(), + ) + })?; + + // Remove the wallet part from the name + let account_name_without_wallet = + String::from(account_name.split_once("/").map(|(_, n)| n).unwrap_or_default()); + + let delegation = self + .insert_proxy_account(proxy_key, consensus_pubkey, module_id, Account { + wallet: consensus_account.wallet.clone(), + name: account_name_without_wallet.clone(), + public_key: Some(proxy_key), + hosts: vec![HostInfo { + url: first_host.url.clone(), + participant_id: first_host.participant_id, + }], + wallet_type: WalletType::Simple, + signing_threshold: 1, + is_proxy: true, + }) + .await?; + + // Unlock the account for immediate use + self.unlock_account(account_name, new_password).await?; + + Ok(delegation) + } + WalletType::Distributed => { + // Pick the first available host to generate the key, Dirk will handle the + // peers. + let host = consensus_account_info.hosts.first().ok_or_else(|| { + SignerModuleError::Internal( + "No hosts available for consensus account".to_string(), + ) + })?; + let channel = self.channels.get(&host.url).cloned().ok_or_else(|| { + SignerModuleError::Internal(format!("No channel found for host {}", host.url)) + })?; + + trace!(host = host.url, "Sending generate request for distributed proxy key"); + let proxy_key = make_generate_proxy_request( + GenerateRequest { + account: account_name.clone(), + passphrase: new_password.as_bytes().to_vec(), + participants: consensus_account_info.hosts.len() as u32, + signing_threshold: consensus_account_info.signing_threshold, + }, + channel.clone(), + ) + .await?; + // Store the password for future use + self.store_password(account_name.clone(), new_password.clone())?; + + let consensus_name = consensus_account_info.name; + let delegation = self + .insert_proxy_account(proxy_key, consensus_pubkey, module_id.clone(), Account { + wallet: consensus_account_info.wallet.clone(), + name: format!("{consensus_name}/{module_id}/{uuid}"), + public_key: Some(proxy_key), + hosts: consensus_account_info.hosts.clone(), + wallet_type: WalletType::Distributed, + signing_threshold: consensus_account_info.signing_threshold, + is_proxy: true, + }) + .await?; + + Ok(delegation) + } + } + } + + async fn insert_proxy_account( + &mut self, + proxy_key: BlsPublicKey, + delegator: BlsPublicKey, + module_id: ModuleId, + account: Account, + ) -> Result, SignerModuleError> { + let hashmap_key = hex::encode(proxy_key); + self.accounts.insert(hashmap_key.clone(), account); + let added = self.accounts.get(&hashmap_key).ok_or(SignerModuleError::Internal( + "Failed to add new proxy account to accounts map".to_string(), + ))?; + trace!(?hashmap_key, ?added, "Proxy account added"); + + // Get delegation signature from the consensus account + let message = ProxyDelegation { delegator, proxy: proxy_key }; + let signature = self.request_signature(delegator, message.tree_hash_root().0, true).await?; + let delegation: SignedProxyDelegation = + SignedProxyDelegation { message, signature }; + + if let Some(store) = &self.proxy_store { + store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { + SignerModuleError::Internal(format!("error storing delegation signature: {err}")) + })?; + } + + Ok(delegation) + } + + pub async fn request_signature( + &self, + pubkey: BlsPublicKey, + object_root: [u8; 32], + is_consensus: bool, + ) -> Result { + let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); + let account = if is_consensus { + self.get_consensus_account(pubkey) + .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))? + .clone() + } else { + self.get_proxy_account(pubkey) + .ok_or_else(|| SignerModuleError::UnknownProxySigner(pubkey.to_vec()))? + .clone() + }; + + match account.wallet_type { + WalletType::Simple => { + let channel = self.get_channel_for_pubkey(&pubkey)?; + self.sign_with_channel(channel, pubkey, &account, domain, object_root).await + } + WalletType::Distributed => { + let num_hosts_needed = account.signing_threshold as usize; + + if account.hosts.len() < num_hosts_needed { + return Err(SignerModuleError::Internal(format!( + "Not enough hosts available. Need {} but only have {}", + num_hosts_needed, + account.hosts.len() + ))); + } + + let mut set = JoinSet::new(); + + // Spawn tasks for each host + for host in &account.hosts { + let Some(channel) = self.channels.get(&host.url).cloned() else { + continue; + }; + let dirk = self.clone(); + let account = account.clone(); + let server_url = host.url.clone(); + let participant_id = host.participant_id; + + set.spawn(async move { + trace!(host = server_url, "Requesting signature shard for creating proxy"); + + match dirk + .sign_with_channel(channel, pubkey, &account, domain, object_root) + .await + { + Ok(signature) => { + trace!( + host = server_url, + participant_id = participant_id, + "Got signature shard" + ); + Ok((signature, participant_id)) + } + Err(e) => { + warn!("Failed to get signature from host {}: {}", server_url, e); + Err(e) + } + } + }); + } + + let mut signatures = Vec::new(); + let mut identifiers = Vec::new(); + + // Collect results until we have enough signatures + while let Some(result) = set.join_next().await { + // Check if we already have enough signatures before processing more + if signatures.len() >= num_hosts_needed { + trace!( + "Already have enough signatures ({}/{}), cancelling remaining tasks", + signatures.len(), + num_hosts_needed + ); + set.abort_all(); + break; + } + + if let Ok(Ok((signature, id))) = result { + signatures.push(signature); + identifiers.push(id); + trace!("Got signature {}/{}", signatures.len(), num_hosts_needed); + + if signatures.len() >= num_hosts_needed { + trace!("Already have enough signatures ({}/{}), cancelling remaining tasks", + signatures.len(), num_hosts_needed); + set.abort_all(); + break; + } + } + } + + if signatures.len() < num_hosts_needed { + return Err(SignerModuleError::Internal(format!( + "Could not collect enough signatures. Need {} but only got {}", + num_hosts_needed, + signatures.len() + ))); + } + + trace!( + num_shards = signatures.len(), + ?identifiers, + "Recovering master signature from shards" + ); + + aggregate_partial_signatures(&signatures, &identifiers).ok_or_else(|| { + SignerModuleError::Internal( + "Failed to recover master signature from shards".to_string(), + ) + }) + } + } + } + + // Helper method to sign with a specific channel + async fn sign_with_channel( + &self, + channel: Channel, + pubkey: BlsPublicKey, + account: &Account, + domain: [u8; 32], + object_root: [u8; 32], + ) -> Result { + let id = match account.wallet_type { + WalletType::Simple => SignerId::PublicKey(pubkey.to_vec()), + WalletType::Distributed => SignerId::Account(account.complete_name()), + }; + + trace!( + %pubkey, + ?id, + is_proxy = account.is_proxy, + wallet_type = ?account.wallet_type, + "Sending Signer/Sign request to Dirk" + ); + + let mut signer_client = SignerClient::new(channel.clone()); + let sign_request = tonic::Request::new(SignRequest { + id: Some(id.clone()), + domain: domain.to_vec(), + data: object_root.to_vec(), + }); + + let sign_response = signer_client + .sign(sign_request) + .await + .map_err(|err| DirkCommunicationError(format!("error on sign request: {err}")))?; + + // Retry if unlock config is set + let sign_response = match sign_response.get_ref().state() { + ResponseState::Denied if self.unlock => { + info!("Failed to sign message, account {pubkey:#} may be locked. Unlocking and retrying."); + + let account_name = account.complete_name(); + self.unlock_account( + account_name.clone(), + self.read_password(account_name.clone())?, + ) + .await?; + + let sign_request = tonic::Request::new(SignRequest { + id: Some(id), + domain: domain.to_vec(), + data: object_root.to_vec(), + }); + signer_client.sign(sign_request).await.map_err(|err| { + DirkCommunicationError(format!("error on sign request: {err}")) + })? + } + _ => sign_response, + }; + + if sign_response.get_ref().state() != ResponseState::Succeeded { + return Err(DirkCommunicationError("sign request returned error".to_string())); + } + + Ok(BlsSignature::from( + FixedBytes::try_from(sign_response.into_inner().signature.as_slice()).map_err( + |_| DirkCommunicationError("return value is not a valid signature".to_string()), + )?, + )) + } +} + +/// Get the accounts for the wallets passed as argument +async fn get_accounts_in_wallets( + channel: Channel, + wallets: Vec, +) -> Result<(Vec, Vec), SignerModuleError> { + trace!(?wallets, "Sending Lister/ListAccounts request to Dirk"); + + let mut client = ListerClient::new(channel); + let pubkeys_request = tonic::Request::new(ListAccountsRequest { paths: wallets }); + let pubkeys_response = client + .list_accounts(pubkeys_request) + .await + .map_err(|err| DirkCommunicationError(format!("error listing accounts: {err}")))?; + + if pubkeys_response.get_ref().state() != ResponseState::Succeeded { + return Err(DirkCommunicationError("list accounts request returned error".to_string())); + } + + let inner = pubkeys_response.into_inner(); + Ok((inner.accounts, inner.distributed_accounts)) +} + +pub fn aggregate_partial_signatures( + partials: &[BlsSignature], + identifiers: &[u64], +) -> Option { + trace!("Aggregating partial signatures"); + // Ensure the number of partial signatures matches the number of identifiers + if partials.len() != identifiers.len() { + warn!("aggregate_partial_signatures: Invalid number of partial signatures"); + return None; + } + + // Deserialize partial signatures into G2 points + let mut points = Vec::new(); + for sig in partials { + if sig.len() != BLS_SIGNATURE_BYTES_LEN { + warn!("aggregate_partial_signatures: Invalid signature length"); + return None; + } + let arr: [u8; BLS_SIGNATURE_BYTES_LEN] = (*sig).into(); + let opt: Option = G2Affine::from_compressed(&arr).into(); + let opt: Option = G2Projective::from(&opt.unwrap()).into(); + if let Some(point) = opt { + points.push(point); + } else { + warn!("aggregate_partial_signatures: Failed to deserialize signature"); + return None; + } + } + + // Create a map of identifiers to their corresponding points + let mut shares: HashMap = HashMap::new(); + for (id, point) in identifiers.iter().zip(points.iter()) { + shares.insert(*id, point); + } + + // Perform Lagrange interpolation to recover the master signature + let mut recovered = G2Projective::identity(); + for (id, point) in &shares { + // Compute the Lagrange coefficient for this identifier + let mut numerator = Scalar::from(1u32); + let mut denominator = Scalar::from(1u32); + for other_id in shares.keys() { + if other_id != id { + numerator *= Scalar::from(*other_id); + denominator *= Scalar::from(*other_id) - Scalar::from(*id); + } + } + let lagrange_coeff = numerator * denominator.invert().unwrap(); + + // Multiply the point by the Lagrange coefficient and add to the recovered point + recovered += **point * lagrange_coeff; + } + + // Serialize the recovered point back into a BlsSignature + let bytes = recovered.to_compressed(); + Some(bytes.into()) +} + +async fn make_generate_proxy_request( + request: GenerateRequest, + channel: Channel, +) -> Result { + let mut client = AccountManagerClient::new(channel.clone()); + let response = client + .generate(request) + .await + .map_err(|err| DirkCommunicationError(format!("error on generate request: {err}")))?; + if response.get_ref().state() != ResponseState::Succeeded { + return Err(DirkCommunicationError("generate request returned error".to_string())); + } + trace!(?response, "Generated new proxy key"); + let proxy_key = + BlsPublicKey::try_from(response.into_inner().public_key.as_slice()).map_err(|_| { + DirkCommunicationError("return value is not a valid public key".to_string()) + })?; + Ok(proxy_key) +} + +/// Checks if a key name follows the proxy pattern +/// // +fn is_proxy_key_name(key_name: &str) -> bool { + key_name.split('/').count() == 3 && + uuid::Uuid::parse_str(key_name.split('/').last().unwrap_or_default()).is_ok() +} + +async fn connect(server: ServerInfo, certs: CertConfig) -> eyre::Result { + let mut tls_config = ClientTlsConfig::new().identity(certs.client_cert); + if let Some(ca) = certs.ca_cert { + tls_config = tls_config.ca_certificate(ca); + } + + if let Some(server_name) = server.server_name { + tls_config = tls_config.domain_name(server_name); + } + + Channel::from_shared(server.endpoint.to_string()) + .map_err(|e| eyre::eyre!(e))? + .tls_config(tls_config) + .map_err(|e| eyre::eyre!(e))? + .connect() + .await + .map_err(|e| eyre::eyre!(e)) +} From 3f59b795028d9651e459d0a5f12faa2dec4be22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 27 Feb 2025 22:44:17 +0000 Subject: [PATCH 084/104] Improve generate proxy key on distributed account --- crates/signer/src/manager/dirk.rs | 90 +-- crates/signer/src/manager/dirk2.rs | 1021 ---------------------------- 2 files changed, 49 insertions(+), 1062 deletions(-) delete mode 100644 crates/signer/src/manager/dirk2.rs diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 38a636a4..d3475c36 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -5,9 +5,7 @@ use alloy::{ }; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ - commit::request::{ - ConsensusProxyMap, ProxyDelegation, ProxyDelegationBls, SignedProxyDelegation, - }, + commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, config::{DirkConfig, DirkHostConfig}, constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, @@ -17,7 +15,7 @@ use cb_common::{ use eyre::bail; use rand::Rng; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, warn}; use tree_hash::TreeHash; use crate::{ @@ -482,48 +480,58 @@ impl DirkManager { let uuid = uuid::Uuid::new_v4(); let password = random_password(); - // TODO: Improve this - let channel = self.connections.get(consensus.participants.get(&1).unwrap()).unwrap(); - let response = AccountManagerClient::new(channel.clone()) - .generate(GenerateRequest { - account: format!("{}/{}/{module}/{uuid}", consensus.wallet, consensus.name), - passphrase: password.as_bytes().to_vec(), - participants: consensus.participants.len() as u32, - signing_threshold: consensus.threshold, - }) - .await - .map_err(|e| SignerModuleError::DirkCommunicationError(e.to_string()))?; + for participant in consensus.participants.values() { + let channel = self.connections.get(participant).unwrap(); + let Ok(response) = AccountManagerClient::new(channel.clone()) + .generate(GenerateRequest { + account: format!("{}/{}/{module}/{uuid}", consensus.wallet, consensus.name), + passphrase: password.as_bytes().to_vec(), + participants: consensus.participants.len() as u32, + signing_threshold: consensus.threshold, + }) + .await + .map_err(|e| { + SignerModuleError::DirkCommunicationError(e.to_string()); + }) + else { + warn!("Couldn't generate proxy key with server {participant}"); + continue; + }; - if response.get_ref().state() != ResponseState::Succeeded { - return Err(SignerModuleError::DirkCommunicationError(format!( - "Failed to generate proxy key: {}", - response.get_ref().message - ))); - } + if response.get_ref().state() != ResponseState::Succeeded { + warn!("Couldn't generate proxy key with server {participant}"); + continue; + } - let proxy_key = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) - .map_err(|_| { - SignerModuleError::DirkCommunicationError("Failed to parse proxy key".to_string()) - })?; + let Ok(proxy_key) = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) + else { + warn!("Failed to parse proxy key from server {participant}"); + continue; + }; - let proxy_account = ProxyAccount { - consensus: Account::Distributed(consensus.clone()), - module: module.clone(), - inner: Account::Distributed(DistributedAccount { - composite_public_key: proxy_key, - participants: consensus.participants.clone(), - threshold: consensus.threshold, - wallet: consensus.wallet.clone(), - name: format!("{}/{module}/{uuid}", consensus.name), - }), - }; + let proxy_account = ProxyAccount { + consensus: Account::Distributed(consensus.clone()), + module: module.clone(), + inner: Account::Distributed(DistributedAccount { + composite_public_key: proxy_key, + participants: consensus.participants.clone(), + threshold: consensus.threshold, + wallet: consensus.wallet.clone(), + name: format!("{}/{module}/{uuid}", consensus.name), + }), + }; - self.store_password(&proxy_account, password).map_err(|e| { - error!("Failed to store password: {e}"); - SignerModuleError::Internal("Failed to store password".to_string()) - })?; + self.store_password(&proxy_account, password).map_err(|e| { + error!("Failed to store password: {e}"); + SignerModuleError::Internal("Failed to store password".to_string()) + })?; - Ok(proxy_account) + return Ok(proxy_account); + } + + return Err(SignerModuleError::DirkCommunicationError( + "All participant connections failed".to_string(), + )) } fn store_password(&self, account: &ProxyAccount, password: String) -> eyre::Result<()> { diff --git a/crates/signer/src/manager/dirk2.rs b/crates/signer/src/manager/dirk2.rs deleted file mode 100644 index 3597e817..00000000 --- a/crates/signer/src/manager/dirk2.rs +++ /dev/null @@ -1,1021 +0,0 @@ -use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf}; - -use alloy::{ - hex, primitives::FixedBytes, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN, - transports::http::reqwest::Url, -}; -use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; -use cb_common::{ - commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, - config::DirkConfig, - constants::COMMIT_BOOST_DOMAIN, - signature::compute_domain, - signer::{BlsPublicKey, BlsSignature, ProxyStore}, - types::{Chain, ModuleId}, -}; -use rand::Rng; -use tokio::task::JoinSet; -use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; -use tracing::{info, trace, warn}; -use tree_hash::TreeHash; - -use crate::{ - error::SignerModuleError::{self, DirkCommunicationError}, - proto::v1::{ - account_manager_client::AccountManagerClient, lister_client::ListerClient, - sign_request::Id as SignerId, signer_client::SignerClient, Account as DirkAccount, - DistributedAccount, GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, - UnlockAccountRequest, - }, -}; - -#[derive(Clone, Debug)] -enum WalletType { - Simple, - Distributed, -} - -#[derive(Clone, Debug)] -struct HostInfo { - url: String, - participant_id: u64, -} - -#[derive(Clone, Debug)] -struct Account { - wallet: String, - name: String, - public_key: Option, - hosts: Vec, - wallet_type: WalletType, - signing_threshold: u32, - is_proxy: bool, -} - -impl Account { - pub fn complete_name(&self) -> String { - format!("{}/{}", self.wallet, self.name) - } -} - -#[derive(Clone, Debug)] -struct ModuleConsensusProxyMap { - pub consensus: BlsPublicKey, - pub proxy_bls: Vec<(BlsPublicKey, ModuleId)>, -} - -struct ServerInfo { - endpoint: Url, - server_name: Option, -} - -#[derive(Clone, Debug)] -struct CertConfig { - client_cert: Identity, - ca_cert: Option, -} - -#[derive(Clone, Debug)] -pub struct DirkManager { - chain: Chain, - certs: CertConfig, - channels2: HashMap, // endpoint -> channel - channels: HashMap, // server_name -> channel - accounts: HashMap, // pubkey -> account - unlock: bool, - secrets_path: PathBuf, - proxy_store: Option, - proxy_maps: Vec, -} - -impl DirkManager { - pub async fn new_from_config(chain: Chain, config: DirkConfig) -> eyre::Result { - let mut tls_configs = Vec::with_capacity(config.hosts.len()); - - // Create a TLS config for each host - for host in config.hosts.clone() { - let mut tls_config = ClientTlsConfig::new().identity(config.client_cert.clone()); - - if let Some(ca) = &config.cert_auth { - tls_config = tls_config.ca_certificate(ca.clone()); - } - - if let Some(server_name) = host.server_name.clone() { - tls_config = tls_config.domain_name(server_name); - tls_configs.push(tls_config); - } else { - trace!("Server name for host {0} not present", host.url); - } - } - - // Create a channel for each host, attempt to connect, - // and save it into a hashmap Host->Channel - let mut channels = HashMap::with_capacity(tls_configs.len()); - for (i, tls_config) in tls_configs.iter().enumerate() { - let config_host = config.hosts[i].clone(); - let url = config_host.url.to_string(); - match Channel::from_shared(url.clone()) - .map_err(|_| eyre::eyre!("Invalid Dirk URL"))? - .tls_config(tls_config.clone()) - .map_err(|_| eyre::eyre!("Invalid Dirk TLS config"))? - .connect() - .await - { - Ok(ch) => { - channels.insert(url.clone(), ch); - } - Err(e) => { - trace!("Couldn't connect to Dirk host {}: {e}", url); - } - } - } - - let mut accounts: HashMap = HashMap::new(); - - for host in config.hosts.clone() { - let url = host.url.to_string(); - let channel = channels - .get(&url) - .ok_or(eyre::eyre!("Couldn't connect to Dirk host {url}"))? - .clone(); - - let (dirk_accounts, dirk_distributed_accounts) = get_accounts_in_wallets( - channel.clone(), - host.accounts - .iter() - .filter_map(|account| Some(account.split_once("/")?.0.to_string())) - .collect(), - ) - .await?; - - for account_name in host.accounts.clone() { - let (wallet, _) = account_name.split_once("/").ok_or(eyre::eyre!( - "Invalid account name: {account_name}. It must be in format wallet/account" - ))?; - - // Handle simple accounts - for dirk_account in dirk_accounts.iter().filter(|a| { - a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) - }) { - let public_key = BlsPublicKey::try_from(dirk_account.public_key.as_slice())?; - let key_name = dirk_account - .name - .split_once("/") - .map(|(_wallet, name)| name) - .unwrap_or_default(); - trace!(?dirk_account.name, "Adding account to hashmap"); - - let is_proxy = is_proxy_key_name(key_name); - - accounts.insert(hex::encode(public_key), Account { - wallet: wallet.to_string(), - name: key_name.to_string(), - public_key: Some(public_key), - hosts: vec![HostInfo { url: url.clone(), participant_id: 1 }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy, - }); - } - - // Handle distributed accounts - for dist_account in dirk_distributed_accounts.iter().filter(|a| { - a.name == account_name || a.name.starts_with(&format!("{}/", account_name)) - }) { - let public_key = - BlsPublicKey::try_from(dist_account.composite_public_key.as_slice())?; - let key_name = dist_account - .name - .split_once("/") - .map(|(_wallet, name)| name) - .unwrap_or_default(); - let is_proxy = is_proxy_key_name(key_name); - - trace!(?dist_account.name, "Adding distributed account to hashmap"); - - // Find the participant ID for this host from the participants list - let url_hostname = host - .url - .host_str() - .ok_or_else(|| eyre::eyre!("Invalid Dirk host {}", host.url))?; - trace!(%url_hostname, "url_hostname"); - let current_participant = - dist_account.participants.first().ok_or_else(|| { - eyre::eyre!( - "Host {} not found in distributed account participants", - url - ) - })?; - - accounts - .entry(hex::encode(public_key)) - .and_modify(|account| { - if !account.hosts.iter().any(|host| host.url == url) { - account.hosts.push(HostInfo { - url: url.clone(), - participant_id: current_participant.id, - }); - } - }) - .or_insert_with(|| Account { - wallet: wallet.to_string(), - name: key_name.to_string(), - public_key: Some(public_key), - hosts: vec![HostInfo { - url: url.clone(), - participant_id: current_participant.id, - }], - wallet_type: WalletType::Distributed, - signing_threshold: dist_account.signing_threshold, - is_proxy, - }); - } - } - } - - let mut channels2 = HashMap::new(); - let certs = CertConfig { - client_cert: config.client_cert.clone(), - ca_cert: config.cert_auth.clone(), - }; - for host in config.hosts { - let server = ServerInfo { endpoint: host.url.clone(), server_name: host.server_name }; - let channel = match connect(server, certs.clone()).await { - Ok(channel) => channel, - Err(e) => { - warn!("Couldn't connect to Dirk server {}: {e}", host.url); - continue; - } - }; - - channels2.insert(host.url.clone(), channel.clone()); - - for account in host.accounts { - let Some((wallet, name)) = account.split_once('/') else { - warn!("Invalid account name: {account}"); - continue; - }; - - let res = ListerClient::new(channel.clone()) - .list_accounts(ListAccountsRequest { paths: vec![account.clone()] }) - .await - .unwrap(); - - if let Some(distributed_account) = res.get_ref().distributed_accounts.get(0) { - let pubkey = distributed_account.composite_public_key.clone(); - let acc = Account { - wallet: wallet.to_string(), - name: name.to_string(), - public_key: Some(BlsPublicKey::try_from(pubkey.as_slice()).unwrap()), - hosts: distributed_account - .participants - .iter() - .map(|participant| HostInfo { - url: format!("{}/{}", participant.name, participant.port), - participant_id: participant.id, - }) - .collect(), - wallet_type: WalletType::Distributed, - signing_threshold: distributed_account.signing_threshold, - is_proxy: is_proxy_key_name(&account), - }; - accounts.insert(account, acc); - } else if let Some(simple_account) = res.get_ref().accounts.get(0) { - let pubkey = simple_account.public_key.clone(); - let acc = Account { - wallet: wallet.to_string(), - name: name.to_string(), - public_key: Some(BlsPublicKey::try_from(pubkey.as_slice()).unwrap()), - hosts: vec![HostInfo { url: host.url.to_string(), participant_id: 1 }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy: is_proxy_key_name(&account), - }; - accounts.insert(account, acc); - } else { - warn!("Account {account} not found in server {}", host.url); - } - } - } - - let mut manager = Self { - chain, - certs: CertConfig { client_cert: config.client_cert, ca_cert: config.cert_auth }, - channels2, - channels, - accounts, - unlock: config.unlock, - secrets_path: config.secrets_path, - proxy_store: None, - proxy_maps: vec![], - }; - manager.build_consensus_proxy_map(); - - Ok(manager) - } - - fn accounts(&self) -> Vec { - self.accounts.values().cloned().collect() - } - - fn consensus_accounts(&self) -> Vec { - self.accounts.values().filter(|a| !a.is_proxy).cloned().collect() - } - - pub fn with_proxy_store(self, proxy_store: ProxyStore) -> eyre::Result { - Ok(Self { proxy_store: Some(proxy_store), ..self }) - } - - fn get_consensus_account(&self, pubkey: BlsPublicKey) -> Option { - self.accounts.get(&hex::encode(pubkey)).filter(|acc| !acc.is_proxy).cloned() - } - - fn get_proxy_account(&self, pubkey: BlsPublicKey) -> Option { - self.accounts.get(&hex::encode(pubkey)).filter(|acc| acc.is_proxy).cloned() - } - - /// Returns the public keys of the config-registered accounts - pub async fn consensus_pubkeys(&self) -> Vec { - self.consensus_accounts() - .iter() - .filter_map(|account| account.public_key) - .collect::>() - } - - /// Returns the public keys of all the proxy accounts found in Dirk. - /// An account is considered a proxy if its name has the format - /// `consensus_account/module_id/uuid`, where `consensus_account` is the - /// name of a config-registered account. - pub async fn proxies(&self) -> Vec { - self.accounts() - .iter() - .filter(|account| account.is_proxy) - .filter_map(|account| account.public_key) - .collect::>() - } - - pub fn get_consensus_proxy_maps(&self, module_id: &ModuleId) -> Vec { - // Filter only proxy keys whose module_id match the request one - // and also remove the module_id part from the struct, since the response - // does not include it. - self.proxy_maps - .iter() - .map(|map| { - let filtered_proxy_bls: Vec = map - .proxy_bls - .iter() - .filter_map(|(pubkey, id)| if id == module_id { Some(*pubkey) } else { None }) - .collect(); - - ConsensusProxyMap { - consensus: map.consensus, - proxy_bls: filtered_proxy_bls, - proxy_ecdsa: vec![], - } - }) - .collect() - } - - /// Builds a mapping of the proxy accounts' pubkeys by consensus account, - /// for a given module. - /// An account is considered a proxy if its name has the format - /// `consensus_account/module_id/uuid`, where `consensus_account` is the - /// name of a config-registered account. - pub fn build_consensus_proxy_map(&mut self) { - let accounts = self.accounts(); - let proxy_accounts: Vec<_> = accounts.iter().filter(|account| account.is_proxy).collect(); - - for consensus_account in self.consensus_accounts() { - let Some(consensus_key) = consensus_account.public_key else { - continue; - }; - let mut proxy_bls: Vec<(BlsPublicKey, ModuleId)> = vec![]; - - let start_of_proxy_name = format!("{}/", consensus_account.complete_name()); - for proxy in &proxy_accounts { - if proxy.complete_name().starts_with(&start_of_proxy_name) && - is_proxy_key_name(&proxy.name) - { - if let Some(pubkey) = proxy.public_key { - // Extract module_id from the proxy account's complete name - let module_id = ModuleId( - proxy.complete_name().split('/').nth(2).unwrap_or_default().to_string(), - ); - proxy_bls.push((pubkey, module_id)); - } - } - } - - self.proxy_maps.push(ModuleConsensusProxyMap { consensus: consensus_key, proxy_bls }); - } - } - - /// Generate a random password of 64 hex-characters - fn random_password() -> String { - let password_bytes: [u8; 32] = rand::thread_rng().gen(); - hex::encode(password_bytes) - } - - /// Read the password for an account from a file - fn read_password(&self, account: String) -> Result { - let path = self.secrets_path.join(format!("{account}.pass")); - trace!(path = ?path, "Reading password from file"); - fs::read_to_string(self.secrets_path.join(format!("{account}.pass"))).map_err(|err| { - SignerModuleError::Internal(format!( - "error reading password for account '{account}': {err}" - )) - }) - } - - /// Store the password for an account in a file - fn store_password(&self, account: String, password: String) -> Result<(), SignerModuleError> { - let account_dir = self - .secrets_path - .join( - account - .rsplit_once("/") - .ok_or(SignerModuleError::Internal(format!( - "account name '{account}' is invalid" - )))? - .0, - ) - .to_string_lossy() - .to_string(); - trace!(%account_dir, "Storing password in file"); - - fs::create_dir_all(account_dir.clone()).map_err(|err| { - SignerModuleError::Internal(format!("error creating dir '{account_dir}': {err}")) - })?; - fs::write(self.secrets_path.join(format!("{account}.pass")), password).map_err(|err| { - SignerModuleError::Internal(format!( - "error writing password for account '{account}': {err}" - )) - }) - } - - /// Get the associated channel for a public key - fn get_channel_for_pubkey(&self, pubkey: &BlsPublicKey) -> Result { - trace!(%pubkey, "Getting channel for public key"); - let key = hex::encode(pubkey); - let account = self - .accounts - .get(&key) - .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; - - // Try to find any available host's channel - for host in &account.hosts { - if let Some(channel) = self.channels.get(&host.url) { - return Ok(channel.clone()); - } - } - - Err(SignerModuleError::Internal("No available channel found for any host".to_string())) - } - - /// Unlock an account. For distributed accounts this is done for all it's - /// hosts. - async fn unlock_account( - &self, - account: String, - password: String, - ) -> Result<(), SignerModuleError> { - let account_entry = - self.accounts.values().find(|a| a.complete_name() == account).ok_or_else(|| { - SignerModuleError::Internal(format!("Account not found: {}", account)) - })?; - - match account_entry.wallet_type { - WalletType::Distributed => { - // For distributed accounts, unlock on all hosts - for host in &account_entry.hosts { - if let Some(channel) = self.channels.get(&host.url) { - self.unlock_account_on_channel(channel, &account, &password).await?; - } - } - } - WalletType::Simple => { - // For simple accounts, unlock on a single host - let channel = - self.get_channel_for_pubkey(account_entry.public_key.as_ref().ok_or_else( - || SignerModuleError::Internal("Account has no public key".to_string()), - )?)?; - self.unlock_account_on_channel(&channel, &account, &password).await?; - } - } - Ok(()) - } - - /// Unlock an account on a specific channel - async fn unlock_account_on_channel( - &self, - channel: &Channel, - account: &str, - password: &str, - ) -> Result<(), SignerModuleError> { - trace!(account, "unlock_account_on_channel"); - const MAX_RETRIES: u32 = 5; - let mut retry_count = 0; - - loop { - let mut client = AccountManagerClient::new(channel.clone()); - let unlock_request = tonic::Request::new(UnlockAccountRequest { - account: account.to_string(), - passphrase: password.as_bytes().to_vec(), - }); - - match client.unlock(unlock_request).await { - Ok(unlock_response) => { - if unlock_response.get_ref().state() == ResponseState::Succeeded { - return Ok(()); - } - // We have connected but an error has been returned - let err = unlock_response.get_ref(); - warn!(?err, "unlock_account_on_channel error response"); - return Err(DirkCommunicationError( - "unlock_account_on_channel error response received".to_string(), - )); - } - Err(status) => { - retry_count += 1; - if retry_count >= MAX_RETRIES { - return Err(DirkCommunicationError(format!( - "Failed to connect after {MAX_RETRIES} attempts: {status}" - ))); - } - - warn!(?status, retry_count, "Connection failed, retrying in 3 seconds..."); - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - } - } - } - } - - pub async fn generate_proxy_key( - &mut self, - module_id: ModuleId, - consensus_pubkey: BlsPublicKey, - ) -> Result, SignerModuleError> { - let Some(consensus_account) = self - .consensus_accounts() - .iter() - .find(|account| { - account.public_key.is_some_and(|account_pk| account_pk == consensus_pubkey) - }) - .map(|account| account.complete_name()) - else { - return Err(SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec())); - }; - - let consensus_account_info = self - .accounts() - .into_iter() - .find(|account| account.complete_name() == consensus_account) - .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()))?; - - let uuid = uuid::Uuid::new_v4(); - let account_name = format!("{consensus_account}/{module_id}/{uuid}"); - let new_password = Self::random_password(); - - match consensus_account_info.wallet_type { - WalletType::Simple => { - trace!(account = account_name, "Sending AccountManager/Generate request to Dirk"); - let channel = self.get_channel_for_pubkey(&consensus_pubkey)?; - let proxy_key = make_generate_proxy_request( - GenerateRequest { - account: account_name.clone(), - passphrase: new_password.as_bytes().to_vec(), - participants: 1, - signing_threshold: 1, - }, - channel.clone(), - ) - .await?; - - // Store the password for future use - self.store_password(account_name.clone(), new_password.clone())?; - - // Get the consensus account info to copy host information - let consensus_account = - self.accounts.get(&hex::encode(consensus_pubkey)).ok_or_else(|| { - SignerModuleError::UnknownConsensusSigner(consensus_pubkey.to_vec()) - })?; - - let first_host = consensus_account.hosts.first().ok_or_else(|| { - SignerModuleError::Internal( - "Consensus account has no associated hosts".to_string(), - ) - })?; - - // Remove the wallet part from the name - let account_name_without_wallet = - String::from(account_name.split_once("/").map(|(_, n)| n).unwrap_or_default()); - - let delegation = self - .insert_proxy_account(proxy_key, consensus_pubkey, module_id, Account { - wallet: consensus_account.wallet.clone(), - name: account_name_without_wallet.clone(), - public_key: Some(proxy_key), - hosts: vec![HostInfo { - url: first_host.url.clone(), - participant_id: first_host.participant_id, - }], - wallet_type: WalletType::Simple, - signing_threshold: 1, - is_proxy: true, - }) - .await?; - - // Unlock the account for immediate use - self.unlock_account(account_name, new_password).await?; - - Ok(delegation) - } - WalletType::Distributed => { - // Pick the first available host to generate the key, Dirk will handle the - // peers. - let host = consensus_account_info.hosts.first().ok_or_else(|| { - SignerModuleError::Internal( - "No hosts available for consensus account".to_string(), - ) - })?; - let channel = self.channels.get(&host.url).cloned().ok_or_else(|| { - SignerModuleError::Internal(format!("No channel found for host {}", host.url)) - })?; - - trace!(host = host.url, "Sending generate request for distributed proxy key"); - let proxy_key = make_generate_proxy_request( - GenerateRequest { - account: account_name.clone(), - passphrase: new_password.as_bytes().to_vec(), - participants: consensus_account_info.hosts.len() as u32, - signing_threshold: consensus_account_info.signing_threshold, - }, - channel.clone(), - ) - .await?; - // Store the password for future use - self.store_password(account_name.clone(), new_password.clone())?; - - let consensus_name = consensus_account_info.name; - let delegation = self - .insert_proxy_account(proxy_key, consensus_pubkey, module_id.clone(), Account { - wallet: consensus_account_info.wallet.clone(), - name: format!("{consensus_name}/{module_id}/{uuid}"), - public_key: Some(proxy_key), - hosts: consensus_account_info.hosts.clone(), - wallet_type: WalletType::Distributed, - signing_threshold: consensus_account_info.signing_threshold, - is_proxy: true, - }) - .await?; - - Ok(delegation) - } - } - } - - async fn insert_proxy_account( - &mut self, - proxy_key: BlsPublicKey, - delegator: BlsPublicKey, - module_id: ModuleId, - account: Account, - ) -> Result, SignerModuleError> { - let hashmap_key = hex::encode(proxy_key); - self.accounts.insert(hashmap_key.clone(), account); - let added = self.accounts.get(&hashmap_key).ok_or(SignerModuleError::Internal( - "Failed to add new proxy account to accounts map".to_string(), - ))?; - trace!(?hashmap_key, ?added, "Proxy account added"); - - // Get delegation signature from the consensus account - let message = ProxyDelegation { delegator, proxy: proxy_key }; - let signature = self.request_signature(delegator, message.tree_hash_root().0, true).await?; - let delegation: SignedProxyDelegation = - SignedProxyDelegation { message, signature }; - - if let Some(store) = &self.proxy_store { - store.store_proxy_bls_delegation(&module_id, &delegation).map_err(|err| { - SignerModuleError::Internal(format!("error storing delegation signature: {err}")) - })?; - } - - Ok(delegation) - } - - pub async fn request_signature( - &self, - pubkey: BlsPublicKey, - object_root: [u8; 32], - is_consensus: bool, - ) -> Result { - let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); - let account = if is_consensus { - self.get_consensus_account(pubkey) - .ok_or_else(|| SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))? - .clone() - } else { - self.get_proxy_account(pubkey) - .ok_or_else(|| SignerModuleError::UnknownProxySigner(pubkey.to_vec()))? - .clone() - }; - - match account.wallet_type { - WalletType::Simple => { - let channel = self.get_channel_for_pubkey(&pubkey)?; - self.sign_with_channel(channel, pubkey, &account, domain, object_root).await - } - WalletType::Distributed => { - let num_hosts_needed = account.signing_threshold as usize; - - if account.hosts.len() < num_hosts_needed { - return Err(SignerModuleError::Internal(format!( - "Not enough hosts available. Need {} but only have {}", - num_hosts_needed, - account.hosts.len() - ))); - } - - let mut set = JoinSet::new(); - - // Spawn tasks for each host - for host in &account.hosts { - let Some(channel) = self.channels.get(&host.url).cloned() else { - continue; - }; - let dirk = self.clone(); - let account = account.clone(); - let server_url = host.url.clone(); - let participant_id = host.participant_id; - - set.spawn(async move { - trace!(host = server_url, "Requesting signature shard for creating proxy"); - - match dirk - .sign_with_channel(channel, pubkey, &account, domain, object_root) - .await - { - Ok(signature) => { - trace!( - host = server_url, - participant_id = participant_id, - "Got signature shard" - ); - Ok((signature, participant_id)) - } - Err(e) => { - warn!("Failed to get signature from host {}: {}", server_url, e); - Err(e) - } - } - }); - } - - let mut signatures = Vec::new(); - let mut identifiers = Vec::new(); - - // Collect results until we have enough signatures - while let Some(result) = set.join_next().await { - // Check if we already have enough signatures before processing more - if signatures.len() >= num_hosts_needed { - trace!( - "Already have enough signatures ({}/{}), cancelling remaining tasks", - signatures.len(), - num_hosts_needed - ); - set.abort_all(); - break; - } - - if let Ok(Ok((signature, id))) = result { - signatures.push(signature); - identifiers.push(id); - trace!("Got signature {}/{}", signatures.len(), num_hosts_needed); - - if signatures.len() >= num_hosts_needed { - trace!("Already have enough signatures ({}/{}), cancelling remaining tasks", - signatures.len(), num_hosts_needed); - set.abort_all(); - break; - } - } - } - - if signatures.len() < num_hosts_needed { - return Err(SignerModuleError::Internal(format!( - "Could not collect enough signatures. Need {} but only got {}", - num_hosts_needed, - signatures.len() - ))); - } - - trace!( - num_shards = signatures.len(), - ?identifiers, - "Recovering master signature from shards" - ); - - aggregate_partial_signatures(&signatures, &identifiers).ok_or_else(|| { - SignerModuleError::Internal( - "Failed to recover master signature from shards".to_string(), - ) - }) - } - } - } - - // Helper method to sign with a specific channel - async fn sign_with_channel( - &self, - channel: Channel, - pubkey: BlsPublicKey, - account: &Account, - domain: [u8; 32], - object_root: [u8; 32], - ) -> Result { - let id = match account.wallet_type { - WalletType::Simple => SignerId::PublicKey(pubkey.to_vec()), - WalletType::Distributed => SignerId::Account(account.complete_name()), - }; - - trace!( - %pubkey, - ?id, - is_proxy = account.is_proxy, - wallet_type = ?account.wallet_type, - "Sending Signer/Sign request to Dirk" - ); - - let mut signer_client = SignerClient::new(channel.clone()); - let sign_request = tonic::Request::new(SignRequest { - id: Some(id.clone()), - domain: domain.to_vec(), - data: object_root.to_vec(), - }); - - let sign_response = signer_client - .sign(sign_request) - .await - .map_err(|err| DirkCommunicationError(format!("error on sign request: {err}")))?; - - // Retry if unlock config is set - let sign_response = match sign_response.get_ref().state() { - ResponseState::Denied if self.unlock => { - info!("Failed to sign message, account {pubkey:#} may be locked. Unlocking and retrying."); - - let account_name = account.complete_name(); - self.unlock_account( - account_name.clone(), - self.read_password(account_name.clone())?, - ) - .await?; - - let sign_request = tonic::Request::new(SignRequest { - id: Some(id), - domain: domain.to_vec(), - data: object_root.to_vec(), - }); - signer_client.sign(sign_request).await.map_err(|err| { - DirkCommunicationError(format!("error on sign request: {err}")) - })? - } - _ => sign_response, - }; - - if sign_response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError("sign request returned error".to_string())); - } - - Ok(BlsSignature::from( - FixedBytes::try_from(sign_response.into_inner().signature.as_slice()).map_err( - |_| DirkCommunicationError("return value is not a valid signature".to_string()), - )?, - )) - } -} - -/// Get the accounts for the wallets passed as argument -async fn get_accounts_in_wallets( - channel: Channel, - wallets: Vec, -) -> Result<(Vec, Vec), SignerModuleError> { - trace!(?wallets, "Sending Lister/ListAccounts request to Dirk"); - - let mut client = ListerClient::new(channel); - let pubkeys_request = tonic::Request::new(ListAccountsRequest { paths: wallets }); - let pubkeys_response = client - .list_accounts(pubkeys_request) - .await - .map_err(|err| DirkCommunicationError(format!("error listing accounts: {err}")))?; - - if pubkeys_response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError("list accounts request returned error".to_string())); - } - - let inner = pubkeys_response.into_inner(); - Ok((inner.accounts, inner.distributed_accounts)) -} - -pub fn aggregate_partial_signatures( - partials: &[BlsSignature], - identifiers: &[u64], -) -> Option { - trace!("Aggregating partial signatures"); - // Ensure the number of partial signatures matches the number of identifiers - if partials.len() != identifiers.len() { - warn!("aggregate_partial_signatures: Invalid number of partial signatures"); - return None; - } - - // Deserialize partial signatures into G2 points - let mut points = Vec::new(); - for sig in partials { - if sig.len() != BLS_SIGNATURE_BYTES_LEN { - warn!("aggregate_partial_signatures: Invalid signature length"); - return None; - } - let arr: [u8; BLS_SIGNATURE_BYTES_LEN] = (*sig).into(); - let opt: Option = G2Affine::from_compressed(&arr).into(); - let opt: Option = G2Projective::from(&opt.unwrap()).into(); - if let Some(point) = opt { - points.push(point); - } else { - warn!("aggregate_partial_signatures: Failed to deserialize signature"); - return None; - } - } - - // Create a map of identifiers to their corresponding points - let mut shares: HashMap = HashMap::new(); - for (id, point) in identifiers.iter().zip(points.iter()) { - shares.insert(*id, point); - } - - // Perform Lagrange interpolation to recover the master signature - let mut recovered = G2Projective::identity(); - for (id, point) in &shares { - // Compute the Lagrange coefficient for this identifier - let mut numerator = Scalar::from(1u32); - let mut denominator = Scalar::from(1u32); - for other_id in shares.keys() { - if other_id != id { - numerator *= Scalar::from(*other_id); - denominator *= Scalar::from(*other_id) - Scalar::from(*id); - } - } - let lagrange_coeff = numerator * denominator.invert().unwrap(); - - // Multiply the point by the Lagrange coefficient and add to the recovered point - recovered += **point * lagrange_coeff; - } - - // Serialize the recovered point back into a BlsSignature - let bytes = recovered.to_compressed(); - Some(bytes.into()) -} - -async fn make_generate_proxy_request( - request: GenerateRequest, - channel: Channel, -) -> Result { - let mut client = AccountManagerClient::new(channel.clone()); - let response = client - .generate(request) - .await - .map_err(|err| DirkCommunicationError(format!("error on generate request: {err}")))?; - if response.get_ref().state() != ResponseState::Succeeded { - return Err(DirkCommunicationError("generate request returned error".to_string())); - } - trace!(?response, "Generated new proxy key"); - let proxy_key = - BlsPublicKey::try_from(response.into_inner().public_key.as_slice()).map_err(|_| { - DirkCommunicationError("return value is not a valid public key".to_string()) - })?; - Ok(proxy_key) -} - -/// Checks if a key name follows the proxy pattern -/// // -fn is_proxy_key_name(key_name: &str) -> bool { - key_name.split('/').count() == 3 && - uuid::Uuid::parse_str(key_name.split('/').last().unwrap_or_default()).is_ok() -} - -async fn connect(server: ServerInfo, certs: CertConfig) -> eyre::Result { - let mut tls_config = ClientTlsConfig::new().identity(certs.client_cert); - if let Some(ca) = certs.ca_cert { - tls_config = tls_config.ca_certificate(ca); - } - - if let Some(server_name) = server.server_name { - tls_config = tls_config.domain_name(server_name); - } - - Channel::from_shared(server.endpoint.to_string()) - .map_err(|e| eyre::eyre!(e))? - .tls_config(tls_config) - .map_err(|e| eyre::eyre!(e))? - .connect() - .await - .map_err(|e| eyre::eyre!(e)) -} From 16ca00d54054e9c3df0a699e34631264488bd89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 11:13:01 +0000 Subject: [PATCH 085/104] Optimize distributed accounts signing waiting times --- Cargo.lock | 1 + crates/signer/Cargo.toml | 1 + crates/signer/src/manager/dirk.rs | 72 ++++++++++++++++++++----------- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3df53c93..4e38ccb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1487,6 +1487,7 @@ dependencies = [ "cb-common", "cb-metrics", "eyre", + "futures", "headers", "lazy_static", "prometheus", diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index adb4e06f..19f4add1 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -21,6 +21,7 @@ prost.workspace = true # async / threads tokio.workspace = true +futures.workspace = true # telemetry tracing.workspace = true diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index d3475c36..306fee11 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -13,6 +13,7 @@ use cb_common::{ types::{Chain, ModuleId}, }; use eyre::bail; +use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; use rand::Rng; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use tracing::{debug, error, warn}; @@ -21,7 +22,7 @@ use tree_hash::TreeHash; use crate::{ error::SignerModuleError, proto::v1::{ - account_manager_client::AccountManagerClient, lister_client::ListerClient, + account_manager_client::AccountManagerClient, lister_client::ListerClient, sign_request, signer_client::SignerClient, Endpoint, GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, }, @@ -311,18 +312,16 @@ impl DirkManager { .sign(SignRequest { data: object_root.to_vec(), domain: domain.to_vec(), - id: Some(crate::proto::v1::sign_request::Id::PublicKey( - account.public_key.to_vec(), - )), + id: Some(sign_request::Id::PublicKey(account.public_key.to_vec())), }) .await - .map_err(|_| { - SignerModuleError::DirkCommunicationError("Failed to sign object".to_string()) + .map_err(|e| { + SignerModuleError::DirkCommunicationError(format!("Failed to sign object: {e}")) })?; if response.get_ref().state() != ResponseState::Succeeded { return Err(SignerModuleError::DirkCommunicationError( - "Failed to sign object".to_string(), + "Failed to sign object, server responded error".to_string(), )); } @@ -331,13 +330,13 @@ impl DirkManager { }) } - // TODO: Improve await times async fn request_distributed_signature( &self, account: &DistributedAccount, object_root: [u8; 32], ) -> Result { let mut partials = Vec::with_capacity(account.participants.len()); + let mut requests = Vec::with_capacity(account.participants.len()); for (id, endpoint) in account.participants.iter() { let Some(channel) = self.connections.get(endpoint) else { @@ -345,19 +344,33 @@ impl DirkManager { continue; }; - let Ok(response) = SignerClient::new(channel.clone()) - .sign(SignRequest { - data: object_root.to_vec(), - domain: compute_domain(self.chain, COMMIT_BOOST_DOMAIN).to_vec(), - id: Some(crate::proto::v1::sign_request::Id::Account(format!( - "{}/{}", - account.wallet, account.name - ))), - }) - .await - else { - warn!("Failed to sign object with server {endpoint}"); - continue; + let request = async move { + SignerClient::new(channel.clone()) + .sign(SignRequest { + data: object_root.to_vec(), + domain: compute_domain(self.chain, COMMIT_BOOST_DOMAIN).to_vec(), + id: Some(sign_request::Id::Account(format!( + "{}/{}", + account.wallet, account.name + ))), + }) + .map(|res| (res, (*id, endpoint.clone()))) + .await + }; + requests.push(request); + } + + let mut requests = requests.into_iter().collect::>(); + + while let Some((response, participant)) = requests.next().await { + let (id, endpoint) = participant; + + let response = match response { + Ok(res) => res, + Err(e) => { + warn!("Failed to sign object with server {endpoint}: {e}"); + continue; + } }; if response.get_ref().state() != ResponseState::Succeeded { @@ -365,13 +378,20 @@ impl DirkManager { continue; } - let Ok(signature) = BlsSignature::try_from(response.into_inner().signature.as_slice()) - else { - warn!("Failed to parse signature from server {endpoint}"); - continue; + let signature = match BlsSignature::try_from(response.into_inner().signature.as_slice()) + { + Ok(sig) => sig, + Err(e) => { + warn!("Failed to parse signature from server {endpoint}: {e}"); + continue; + } }; - partials.push((signature, *id)); + partials.push((signature, id)); + + if partials.len() >= account.threshold as usize { + break; + } } if partials.len() < account.threshold as usize { From dcb24d53e0f5156c9522378301695034fc29c485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 12:01:14 +0000 Subject: [PATCH 086/104] Optimize initial setup reducing requests --- crates/common/src/config/signer.rs | 6 - crates/signer/src/manager/dirk.rs | 235 +++++++++++++++++------------ 2 files changed, 141 insertions(+), 100 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 5866b885..d6bf1eda 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -43,12 +43,6 @@ pub struct DirkHostConfig { pub accounts: Vec, } -impl DirkHostConfig { - pub fn name(&self) -> Option { - self.server_name.clone().or_else(|| self.url.host_str().map(String::from)) - } -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub enum SignerType { diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 306fee11..d4c441ab 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -1,4 +1,8 @@ -use std::{collections::HashMap, io::Write, path::PathBuf}; +use std::{ + collections::{HashMap, HashSet}, + io::Write, + path::PathBuf, +}; use alloy::{ hex, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN, transports::http::reqwest::Url, @@ -101,11 +105,6 @@ impl DirkManager { let mut consensus_accounts = HashMap::new(); for host in config.hosts { - let Some(host_name) = host.name() else { - warn!("Host name not found for server {}", host.url); - continue; - }; - let channel = match connect(&host, &certs).await { Ok(channel) => channel, Err(e) => { @@ -116,93 +115,44 @@ impl DirkManager { connections.insert(host.url.clone(), channel.clone()); - // TODO: Improve to minimize requests - for account_name in host.accounts { - let Ok((wallet, name)) = decompose_name(&account_name) else { - warn!("Invalid account name {account_name}"); - continue; - }; - - let response = match ListerClient::new(channel.clone()) - .list_accounts(ListAccountsRequest { paths: vec![account_name.clone()] }) - .await - { - Ok(res) => res, - Err(e) => { - warn!("Failed to get account {account_name}: {e}"); - continue; - } - }; + let wallets: HashSet = host + .accounts + .iter() + .map(|account| decompose_name(account).unwrap_or_default().0) + .collect(); - if response.get_ref().state() != ResponseState::Succeeded { - warn!("Failed to get account {account_name}"); + let accounts_response = match ListerClient::new(channel.clone()) + .list_accounts(ListAccountsRequest { paths: wallets.into_iter().collect() }) + .await + { + Ok(res) => res, + Err(e) => { + warn!("Failed to list accounts in server {}: {e}", host.url); continue; } + }; - if let Some(account) = response.get_ref().accounts.get(0) { - // The account is Simple - match BlsPublicKey::try_from(account.public_key.as_slice()) { - Ok(public_key) => { - consensus_accounts.insert( - public_key, - Account::Simple(SimpleAccount { - public_key, - server: host.url.clone(), - wallet: wallet.to_string(), - name: name.to_string(), - }), - ); - } - Err(_) => { - warn!("Failed to parse public key for account {account_name}"); - continue; - } - } - } else if let Some(account) = response.get_ref().distributed_accounts.get(0) { - // The account is Distributed - let Ok(public_key) = - BlsPublicKey::try_from(account.composite_public_key.as_slice()) - else { - warn!("Failed to parse composite public key for account {account_name}"); - continue; - }; - - let Some(&Endpoint { id: participant_id, .. }) = account - .participants - .iter() - .find(|participant| participant.name == host_name) - else { - warn!( - "Host {host_name} not found as participant for account {account_name}" - ); - continue; - }; - - match consensus_accounts.get_mut(&public_key) { - Some(Account::Distributed(DistributedAccount { participants, .. })) => { - participants.insert(participant_id as u32, host.url.clone()); - } - Some(Account::Simple(_)) => { - bail!("Distributed public key already exists for simple account"); - } - None => { - let mut participants = - HashMap::with_capacity(account.participants.len()); - participants.insert(participant_id as u32, host.url.clone()); - consensus_accounts.insert( - public_key, - Account::Distributed(DistributedAccount { - composite_public_key: public_key, - participants, - threshold: account.signing_threshold, - wallet: wallet.to_string(), - name: name.to_string(), - }), - ); - } - } - } else { - warn!("Account {account_name} not found in server {}", host.url); + if accounts_response.get_ref().state() != ResponseState::Succeeded { + warn!("Failed to list accounts in server {}", host.url); + continue; + } + + let accounts_response = accounts_response.into_inner(); + load_simple_accounts(accounts_response.accounts, &host, &mut consensus_accounts); + load_distributed_accounts( + accounts_response.distributed_accounts, + &host, + &mut consensus_accounts, + ) + .map_err(|error| warn!("{error}")) + .ok(); + + for account in host.accounts { + if !consensus_accounts + .values() + .any(|account| account.full_name() == account.full_name()) + { + warn!("Account {account} not found in server {}", host.url); } } } @@ -580,13 +530,110 @@ async fn connect(server: &DirkHostConfig, certs: &CertConfig) -> eyre::Result eyre::Result<(&str, &str)> { - full_name.split_once('/').ok_or_else(|| eyre::eyre!("Invalid account name")) +fn decompose_name(full_name: &str) -> eyre::Result<(String, String)> { + full_name + .split_once('/') + .map(|(wallet, name)| (wallet.to_string(), name.to_string())) + .ok_or_else(|| eyre::eyre!("Invalid account name")) +} + +fn load_simple_accounts( + accounts: Vec, + host: &DirkHostConfig, + consensus_accounts: &mut HashMap, +) { + for account in accounts { + if !host.accounts.contains(&account.name) { + continue; + } + + let Ok((wallet, name)) = decompose_name(&account.name) else { + warn!("Invalid account name {}", account.name); + continue; + }; + + match BlsPublicKey::try_from(account.public_key.as_slice()) { + Ok(public_key) => { + consensus_accounts.insert( + public_key, + Account::Simple(SimpleAccount { + public_key, + server: host.url.clone(), + wallet, + name, + }), + ); + } + Err(_) => { + warn!("Failed to parse public key for account {}", account.name); + continue; + } + } + } +} + +fn load_distributed_accounts( + accounts: Vec, + host: &DirkHostConfig, + consensus_accounts: &mut HashMap, +) -> eyre::Result<()> { + let host_name = host + .server_name + .clone() + .or_else(|| host.url.host_str().map(String::from)) + .ok_or(eyre::eyre!("Host name not found for server {}", host.url))?; + + for account in accounts { + if !host.accounts.contains(&account.name) { + continue; + } + + let Ok(public_key) = BlsPublicKey::try_from(account.composite_public_key.as_slice()) else { + warn!("Failed to parse composite public key for account {}", account.name); + continue; + }; + + let Some(&Endpoint { id: participant_id, .. }) = + account.participants.iter().find(|participant| participant.name == host_name) + else { + warn!("Host {host_name} not found as participant for account {}", account.name); + continue; + }; + + match consensus_accounts.get_mut(&public_key) { + Some(Account::Distributed(DistributedAccount { participants, .. })) => { + participants.insert(participant_id as u32, host.url.clone()); + } + None => { + let Ok((wallet, name)) = decompose_name(&account.name) else { + warn!("Invalid account name {}", account.name); + continue; + }; + + let mut participants = HashMap::with_capacity(account.participants.len()); + participants.insert(participant_id as u32, host.url.clone()); + + consensus_accounts.insert( + public_key, + Account::Distributed(DistributedAccount { + composite_public_key: public_key, + participants, + threshold: account.signing_threshold, + wallet, + name, + }), + ); + } + Some(Account::Simple(_)) => { + bail!("Distributed public key already exists for simple account"); + } + } + } + + Ok(()) } -pub fn aggregate_partial_signatures( - partials: &[(BlsSignature, u32)], -) -> eyre::Result { +fn aggregate_partial_signatures(partials: &[(BlsSignature, u32)]) -> eyre::Result { // Deserialize partial signatures into G2 points let mut shares: HashMap = HashMap::new(); for (sig, id) in partials { From f710449ba234d297f96cacb35ccc00da082e8e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 12:01:45 +0000 Subject: [PATCH 087/104] Remove unused fields --- crates/signer/src/manager/dirk.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index d4c441ab..a29c5bf6 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -89,7 +89,6 @@ struct ProxyAccount { #[derive(Clone, Debug)] pub struct DirkManager { chain: Chain, - certs: CertConfig, connections: HashMap, consensus_accounts: HashMap, proxy_accounts: HashMap, @@ -165,7 +164,6 @@ impl DirkManager { Ok(Self { chain, - certs, connections, consensus_accounts, proxy_accounts: HashMap::new(), From d04f5a2c428844d08aced0acfbbe4adcc592874f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 13:11:55 +0000 Subject: [PATCH 088/104] Remove unlock feature --- crates/common/src/config/signer.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index d6bf1eda..3d1dccc9 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -70,9 +70,6 @@ pub enum SignerType { secrets_path: PathBuf, /// Path to the CA certificate ca_cert_path: Option, - /// Whether to unlock the accounts in case they are locked - #[serde(default)] - unlock: bool, /// How to store proxy key delegations /// ERC2335 is not supported with Dirk signer store: Option, @@ -85,7 +82,6 @@ pub struct DirkConfig { pub client_cert: Identity, pub secrets_path: PathBuf, pub cert_auth: Option, - pub unlock: bool, } #[derive(Debug, Clone)] @@ -123,7 +119,6 @@ impl StartSignerConfig { key_path, secrets_path, ca_cert_path, - unlock, store, .. } => { @@ -157,7 +152,6 @@ impl StartSignerConfig { } None => None, }, - unlock, }), }) } From e5b7e19545825918844d0f40cda520a5364a5d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 13:14:04 +0000 Subject: [PATCH 089/104] Update config examples --- config.example.toml | 3 --- examples/configs/dirk_signer.toml | 1 - 2 files changed, 4 deletions(-) diff --git a/config.example.toml b/config.example.toml index 54015675..301a6724 100644 --- a/config.example.toml +++ b/config.example.toml @@ -160,9 +160,6 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # Path to the CA certificate that signed the Dirk server certificate # OPTIONAL # ca_cert_path = "/path/to/ca.crt" -# Whether to try to unlock the accounts on sign failure -# OPTIONAL, DEFAULT: false -# unlock = false # Add one entry like this for each Dirk host # [[signer.dirk.hosts]] diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml index 32787107..cc2c5045 100644 --- a/examples/configs/dirk_signer.toml +++ b/examples/configs/dirk_signer.toml @@ -13,7 +13,6 @@ docker_image = "commitboost_signer" cert_path = "/path/to/client.crt" key_path = "/path/to/client.key" secrets_path = "./dirk_secrets" -unlock = true # Example of a single Dirk host [[signer.dirk.hosts]] From e7b6910540990844d10753f9d65fc764f445e7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 13:21:27 +0000 Subject: [PATCH 090/104] Fix password store --- crates/signer/src/manager/dirk.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index a29c5bf6..76e2011b 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -504,7 +504,8 @@ impl DirkManager { fn store_password(&self, account: &ProxyAccount, password: String) -> eyre::Result<()> { let path = self.secrets_path.join(account.inner.full_name()); - let mut file = std::fs::File::create(path)?; + std::fs::create_dir_all(path.clone())?; + let mut file = std::fs::File::create(path.join("password.txt"))?; file.write_all(password.as_bytes())?; Ok(()) } From 1fbb0755874094caa9ff1ca430bbd5923cc42130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 13:25:48 +0000 Subject: [PATCH 091/104] Improve errors --- crates/signer/src/manager/local.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index 358ef912..59edafa2 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -239,7 +239,7 @@ impl LocalSigningManager { let entry = keys .iter_mut() .find(|x| x.consensus == delegator) - .ok_or(SignerModuleError::Internal("missing consensus".to_string()))?; + .ok_or(SignerModuleError::UnknownConsensusSigner(delegator.0.to_vec()))?; entry.proxy_bls.push(bls); } @@ -249,7 +249,7 @@ impl LocalSigningManager { let entry = keys .iter_mut() .find(|x| x.consensus == delegator) - .ok_or(SignerModuleError::Internal("missing consensus".to_string()))?; + .ok_or(SignerModuleError::UnknownConsensusSigner(delegator.0.to_vec()))?; entry.proxy_ecdsa.push(ecdsa); } From 90ed5345dc1c8a58f498e0c5a6258c5ffea87404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 13:38:08 +0000 Subject: [PATCH 092/104] Update proxy secret store to follow docs --- crates/signer/src/manager/dirk.rs | 12 ++++++++---- docs/docs/get_started/configuration.md | 4 +--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 76e2011b..31f70db6 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -16,7 +16,7 @@ use cb_common::{ signer::{BlsPublicKey, BlsSignature, ProxyStore}, types::{Chain, ModuleId}, }; -use eyre::bail; +use eyre::{bail, OptionExt}; use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; use rand::Rng; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; @@ -503,10 +503,14 @@ impl DirkManager { } fn store_password(&self, account: &ProxyAccount, password: String) -> eyre::Result<()> { - let path = self.secrets_path.join(account.inner.full_name()); - std::fs::create_dir_all(path.clone())?; - let mut file = std::fs::File::create(path.join("password.txt"))?; + let full_name = account.inner.full_name(); + let (parent, name) = full_name.rsplit_once('/').ok_or_eyre("Invalid account name")?; + let parent_path = self.secrets_path.join(parent); + + std::fs::create_dir_all(parent_path.clone())?; + let mut file = std::fs::File::create(parent_path.join(format!("{name}.pass")))?; file.write_all(password.as_bytes())?; + Ok(()) } } diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index c08fbe65..37547611 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -252,7 +252,6 @@ secrets_path = "/path/to/secrets" # Optional parameters ca_cert_path = "/path/to/ca.crt" server_domain = "server.example.com" -unlock = false # Add one entry like this for each host [[signer.dirk.hosts]] @@ -268,8 +267,7 @@ accounts = ["AnotherWallet/AnotherAccount", "DistributedWallet/Account1"] - `cert_path` and `key_path` are the paths to the client certificate and key used to authenticate with Dirk. - `accounts` is a list of accounts that the Signer module will consider as the consensus keys. Each account has the format `/`. Accounts can be from different wallets. Generated proxy keys will have format `///`. -- `secrets_path` is the path to the folder containing the passwords of the accounts. Passwords must be in plain text in files with structure `//.pass`. Generated proxy accounts passwords will be stored in `////.pass`. -- `unlock` is an optional parameter that can be set to `true` if you want to try to unlock the wallets on sign failure. Default is `false`. +- `secrets_path` is the path to the folder containing the passwords of the generated proxy accounts, which will be stored in `////.pass`. Additionally, you can set a proxy store so that the delegation signatures for generated proxy keys are stored locally. As these signatures are not sensitive, the only supported store type is `File`: From eb7e8cbcb835e761f218c88c0362009894b5e5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 13:50:47 +0000 Subject: [PATCH 093/104] Fix docker generation --- crates/cli/src/docker_init.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 00f0eba8..e50bb61b 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -323,9 +323,9 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu }; // setup signer service - match signer_config.inner { - SignerType::Local { loader, store } => { - if needs_signer_module { + if needs_signer_module { + match signer_config.inner { + SignerType::Local { loader, store } => { if metrics_enabled { targets.push(PrometheusTargetConfig { targets: vec![format!("cb_signer:{metrics_port}")], @@ -449,9 +449,7 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu services.insert("cb_signer".to_owned(), Some(signer_service)); } - } - SignerType::Dirk { cert_path, key_path, secrets_path, ca_cert_path, store, .. } => { - if needs_signer_module { + SignerType::Dirk { cert_path, key_path, secrets_path, ca_cert_path, store, .. } => { if metrics_enabled { targets.push(PrometheusTargetConfig { targets: vec![format!("cb_signer:{metrics_port}")], @@ -550,9 +548,9 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu services.insert("cb_signer".to_owned(), Some(signer_service)); } - } - SignerType::Remote { .. } => { - panic!("Signer module required but remote config provided"); + SignerType::Remote { .. } => { + panic!("Signer module required but remote config provided"); + } } } From 86fde1e7a13302378d409a6d3845690df19528a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 14:14:25 +0000 Subject: [PATCH 094/104] Remove unnecessary structs --- crates/signer/src/manager/dirk.rs | 91 ++++++++++++------------------- 1 file changed, 36 insertions(+), 55 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 31f70db6..165b6ca7 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -32,12 +32,6 @@ use crate::{ }, }; -#[derive(Clone, Debug)] -struct CertConfig { - ca: Option, - client: Identity, -} - #[derive(Clone, Debug)] enum Account { Simple(SimpleAccount), @@ -56,7 +50,7 @@ impl Account { #[derive(Clone, Debug)] struct SimpleAccount { public_key: BlsPublicKey, - server: Url, + connection: Channel, wallet: String, name: String, } @@ -64,7 +58,7 @@ struct SimpleAccount { #[derive(Clone, Debug)] struct DistributedAccount { composite_public_key: BlsPublicKey, - participants: HashMap, + participants: HashMap, threshold: u32, wallet: String, name: String, @@ -89,7 +83,6 @@ struct ProxyAccount { #[derive(Clone, Debug)] pub struct DirkManager { chain: Chain, - connections: HashMap, consensus_accounts: HashMap, proxy_accounts: HashMap, secrets_path: PathBuf, @@ -98,13 +91,10 @@ pub struct DirkManager { impl DirkManager { pub async fn new(chain: Chain, config: DirkConfig) -> eyre::Result { - let certs = CertConfig { ca: config.cert_auth, client: config.client_cert }; - - let mut connections = HashMap::with_capacity(config.hosts.len()); let mut consensus_accounts = HashMap::new(); for host in config.hosts { - let channel = match connect(&host, &certs).await { + let channel = match connect(&host, &config.client_cert, &config.cert_auth).await { Ok(channel) => channel, Err(e) => { warn!("Failed to connect to Dirk host {}: {e}", host.url); @@ -112,8 +102,6 @@ impl DirkManager { } }; - connections.insert(host.url.clone(), channel.clone()); - let wallets: HashSet = host .accounts .iter() @@ -137,10 +125,16 @@ impl DirkManager { } let accounts_response = accounts_response.into_inner(); - load_simple_accounts(accounts_response.accounts, &host, &mut consensus_accounts); + load_simple_accounts( + accounts_response.accounts, + &host, + &channel, + &mut consensus_accounts, + ); load_distributed_accounts( accounts_response.distributed_accounts, &host, + &channel, &mut consensus_accounts, ) .map_err(|error| warn!("{error}")) @@ -164,7 +158,6 @@ impl DirkManager { Ok(Self { chain, - connections, consensus_accounts, proxy_accounts: HashMap::new(), secrets_path: config.secrets_path, @@ -251,12 +244,7 @@ impl DirkManager { ) -> Result { let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); - let channel = self - .connections - .get(&account.server) - .ok_or(SignerModuleError::DirkCommunicationError("Unknown Dirk host".to_string()))?; - - let response = SignerClient::new(channel.clone()) + let response = SignerClient::new(account.connection.clone()) .sign(SignRequest { data: object_root.to_vec(), domain: domain.to_vec(), @@ -286,12 +274,7 @@ impl DirkManager { let mut partials = Vec::with_capacity(account.participants.len()); let mut requests = Vec::with_capacity(account.participants.len()); - for (id, endpoint) in account.participants.iter() { - let Some(channel) = self.connections.get(endpoint) else { - warn!("Couldn't find server {endpoint}"); - continue; - }; - + for (id, channel) in account.participants.iter() { let request = async move { SignerClient::new(channel.clone()) .sign(SignRequest { @@ -302,7 +285,7 @@ impl DirkManager { account.wallet, account.name ))), }) - .map(|res| (res, (*id, endpoint.clone()))) + .map(|res| (res, *id)) .await }; requests.push(request); @@ -310,19 +293,17 @@ impl DirkManager { let mut requests = requests.into_iter().collect::>(); - while let Some((response, participant)) = requests.next().await { - let (id, endpoint) = participant; - + while let Some((response, participant_id)) = requests.next().await { let response = match response { Ok(res) => res, Err(e) => { - warn!("Failed to sign object with server {endpoint}: {e}"); + warn!("Failed to sign object with participant {participant_id}: {e}"); continue; } }; if response.get_ref().state() != ResponseState::Succeeded { - warn!("Failed to sign object with server {endpoint}"); + warn!("Failed to sign object with participant {participant_id}"); continue; } @@ -330,12 +311,12 @@ impl DirkManager { { Ok(sig) => sig, Err(e) => { - warn!("Failed to parse signature from server {endpoint}: {e}"); + warn!("Failed to parse signature from participant {participant_id}: {e}"); continue; } }; - partials.push((signature, id)); + partials.push((signature, participant_id)); if partials.len() >= account.threshold as usize { break; @@ -391,15 +372,10 @@ impl DirkManager { consensus: &SimpleAccount, module: &ModuleId, ) -> Result { - let channel = self - .connections - .get(&consensus.server) - .ok_or(SignerModuleError::DirkCommunicationError("Unknown Dirk host".to_string()))?; - let uuid = uuid::Uuid::new_v4(); let password = random_password(); - let response = AccountManagerClient::new(channel.clone()) + let response = AccountManagerClient::new(consensus.connection.clone()) .generate(GenerateRequest { account: format!("{}/{}/{module}/{uuid}", consensus.wallet, consensus.name), passphrase: password.as_bytes().to_vec(), @@ -426,7 +402,7 @@ impl DirkManager { module: module.clone(), inner: Account::Simple(SimpleAccount { public_key: proxy_key, - server: consensus.server.clone(), + connection: consensus.connection.clone(), wallet: consensus.wallet.clone(), name: format!("{}/{module}/{uuid}", consensus.name), }), @@ -448,8 +424,7 @@ impl DirkManager { let uuid = uuid::Uuid::new_v4(); let password = random_password(); - for participant in consensus.participants.values() { - let channel = self.connections.get(participant).unwrap(); + for (id, channel) in consensus.participants.iter() { let Ok(response) = AccountManagerClient::new(channel.clone()) .generate(GenerateRequest { account: format!("{}/{}/{module}/{uuid}", consensus.wallet, consensus.name), @@ -462,18 +437,18 @@ impl DirkManager { SignerModuleError::DirkCommunicationError(e.to_string()); }) else { - warn!("Couldn't generate proxy key with server {participant}"); + warn!("Couldn't generate proxy key with participant {id}"); continue; }; if response.get_ref().state() != ResponseState::Succeeded { - warn!("Couldn't generate proxy key with server {participant}"); + warn!("Couldn't generate proxy key with participant {id}"); continue; } let Ok(proxy_key) = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) else { - warn!("Failed to parse proxy key from server {participant}"); + warn!("Failed to parse proxy key with participant {id}"); continue; }; @@ -515,9 +490,13 @@ impl DirkManager { } } -async fn connect(server: &DirkHostConfig, certs: &CertConfig) -> eyre::Result { - let mut tls_config = ClientTlsConfig::new().identity(certs.client.clone()); - if let Some(ca) = &certs.ca { +async fn connect( + server: &DirkHostConfig, + client: &Identity, + ca: &Option, +) -> eyre::Result { + let mut tls_config = ClientTlsConfig::new().identity(client.clone()); + if let Some(ca) = ca { tls_config = tls_config.ca_certificate(ca.clone()); } if let Some(server_name) = &server.server_name { @@ -543,6 +522,7 @@ fn decompose_name(full_name: &str) -> eyre::Result<(String, String)> { fn load_simple_accounts( accounts: Vec, host: &DirkHostConfig, + channel: &Channel, consensus_accounts: &mut HashMap, ) { for account in accounts { @@ -561,7 +541,7 @@ fn load_simple_accounts( public_key, Account::Simple(SimpleAccount { public_key, - server: host.url.clone(), + connection: channel.clone(), wallet, name, }), @@ -578,6 +558,7 @@ fn load_simple_accounts( fn load_distributed_accounts( accounts: Vec, host: &DirkHostConfig, + channel: &Channel, consensus_accounts: &mut HashMap, ) -> eyre::Result<()> { let host_name = host @@ -605,7 +586,7 @@ fn load_distributed_accounts( match consensus_accounts.get_mut(&public_key) { Some(Account::Distributed(DistributedAccount { participants, .. })) => { - participants.insert(participant_id as u32, host.url.clone()); + participants.insert(participant_id as u32, channel.clone()); } None => { let Ok((wallet, name)) = decompose_name(&account.name) else { @@ -614,7 +595,7 @@ fn load_distributed_accounts( }; let mut participants = HashMap::with_capacity(account.participants.len()); - participants.insert(participant_id as u32, host.url.clone()); + participants.insert(participant_id as u32, channel.clone()); consensus_accounts.insert( public_key, From ae56f9e59d97fdd52a559fcdfb160f6a24d71949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 21:08:48 +0000 Subject: [PATCH 095/104] Unlock proxy account when generated --- crates/signer/src/manager/dirk.rs | 39 ++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 165b6ca7..ef7ea439 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -4,9 +4,7 @@ use std::{ path::PathBuf, }; -use alloy::{ - hex, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN, transports::http::reqwest::Url, -}; +use alloy::{hex, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN}; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, @@ -28,7 +26,7 @@ use crate::{ proto::v1::{ account_manager_client::AccountManagerClient, lister_client::ListerClient, sign_request, signer_client::SignerClient, Endpoint, GenerateRequest, ListAccountsRequest, ResponseState, - SignRequest, + SignRequest, UnlockAccountRequest, }, }; @@ -408,11 +406,13 @@ impl DirkManager { }), }; - self.store_password(&proxy_account, password).map_err(|e| { + self.store_password(&proxy_account, password.clone()).map_err(|e| { error!("Failed to store password: {e}"); SignerModuleError::Internal("Failed to store password".to_string()) })?; + self.try_unlock_account(&proxy_account.inner, password).await; + Ok(proxy_account) } @@ -464,11 +464,13 @@ impl DirkManager { }), }; - self.store_password(&proxy_account, password).map_err(|e| { + self.store_password(&proxy_account, password.clone()).map_err(|e| { error!("Failed to store password: {e}"); SignerModuleError::Internal("Failed to store password".to_string()) })?; + self.try_unlock_account(&proxy_account.inner, password).await; + return Ok(proxy_account); } @@ -488,6 +490,31 @@ impl DirkManager { Ok(()) } + + async fn try_unlock_account(&self, account: &Account, password: String) { + let participants = match account { + Account::Simple(account) => vec![(1, account.connection.clone())], + Account::Distributed(account) => { + account.participants.iter().map(|(id, channel)| (*id, channel.clone())).collect() + } + }; + + let unlock_request = UnlockAccountRequest { + account: account.full_name(), + passphrase: password.as_bytes().to_vec(), + }; + + for (id, channel) in participants { + let response = AccountManagerClient::new(channel) + .unlock(unlock_request.clone()) + .await + .map_err(|e| warn!("Failed to unlock account with participant {id}: {e}")); + + if response.is_ok_and(|res| res.into_inner().state() != ResponseState::Succeeded) { + warn!("Failed to unlock account with partcipant {id}"); + } + } + } } async fn connect( From 7ba267ece44eb318e082fd9d862b053fa594308d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 28 Feb 2025 21:37:13 +0000 Subject: [PATCH 096/104] Optimize unlock requests minimizing wait times --- crates/signer/src/manager/dirk.rs | 66 ++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index ef7ea439..1d157930 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -15,7 +15,7 @@ use cb_common::{ types::{Chain, ModuleId}, }; use eyre::{bail, OptionExt}; -use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; +use futures::{future::join_all, stream::FuturesUnordered, FutureExt, StreamExt}; use rand::Rng; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use tracing::{debug, error, warn}; @@ -411,7 +411,12 @@ impl DirkManager { SignerModuleError::Internal("Failed to store password".to_string()) })?; - self.try_unlock_account(&proxy_account.inner, password).await; + if let Err(e) = self.unlock_account(&proxy_account.inner, password).await { + error!("{e}"); + return Err(SignerModuleError::DirkCommunicationError( + "Failed to unlock new account".to_string(), + )); + } Ok(proxy_account) } @@ -469,7 +474,12 @@ impl DirkManager { SignerModuleError::Internal("Failed to store password".to_string()) })?; - self.try_unlock_account(&proxy_account.inner, password).await; + if let Err(e) = self.unlock_account(&proxy_account.inner, password).await { + error!("{e}"); + return Err(SignerModuleError::DirkCommunicationError( + "Failed to unlock new account".to_string(), + )); + } return Ok(proxy_account); } @@ -491,27 +501,45 @@ impl DirkManager { Ok(()) } - async fn try_unlock_account(&self, account: &Account, password: String) { + async fn unlock_account(&self, account: &Account, password: String) -> eyre::Result<()> { let participants = match account { - Account::Simple(account) => vec![(1, account.connection.clone())], - Account::Distributed(account) => { - account.participants.iter().map(|(id, channel)| (*id, channel.clone())).collect() - } + Account::Simple(account) => vec![&account.connection], + Account::Distributed(account) => account.participants.values().collect(), }; - let unlock_request = UnlockAccountRequest { - account: account.full_name(), - passphrase: password.as_bytes().to_vec(), - }; + let mut requests = Vec::with_capacity(participants.len()); + for channel in participants { + let password = password.clone(); + let request = async move { + let response = AccountManagerClient::new(channel.clone()) + .unlock(UnlockAccountRequest { + account: account.full_name(), + passphrase: password.as_bytes().to_vec(), + }) + .await; - for (id, channel) in participants { - let response = AccountManagerClient::new(channel) - .unlock(unlock_request.clone()) - .await - .map_err(|e| warn!("Failed to unlock account with participant {id}: {e}")); + return response + .is_ok_and(|res| res.into_inner().state() == ResponseState::Succeeded); + }; + + requests.push(request); + } - if response.is_ok_and(|res| res.into_inner().state() != ResponseState::Succeeded) { - warn!("Failed to unlock account with partcipant {id}"); + let responses = join_all(requests).await; + match account { + Account::Simple(_) => { + if responses.get(0).is_some_and(|x| *x) { + Ok(()) + } else { + bail!("Failed to unlock account") + } + } + Account::Distributed(account) => { + if responses.into_iter().filter(|x| *x).count() >= account.threshold as usize { + Ok(()) + } else { + bail!("Failed to get enough unlocks") + } } } } From f85b5d52f7896f1c3ed692a861463219fcce7ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 5 Mar 2025 18:10:11 -0300 Subject: [PATCH 097/104] Fix clippy --- crates/signer/src/manager/dirk.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 1d157930..12a332e7 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -181,8 +181,8 @@ impl DirkManager { pub fn get_consensus_proxy_maps(&self, module: &ModuleId) -> Vec { self.consensus_accounts - .iter() - .map(|(_, account)| ConsensusProxyMap { + .values() + .map(|account| ConsensusProxyMap { consensus: account.public_key(), proxy_bls: self .proxy_accounts @@ -438,9 +438,6 @@ impl DirkManager { signing_threshold: consensus.threshold, }) .await - .map_err(|e| { - SignerModuleError::DirkCommunicationError(e.to_string()); - }) else { warn!("Couldn't generate proxy key with participant {id}"); continue; @@ -484,7 +481,7 @@ impl DirkManager { return Ok(proxy_account); } - return Err(SignerModuleError::DirkCommunicationError( + Err(SignerModuleError::DirkCommunicationError( "All participant connections failed".to_string(), )) } @@ -518,8 +515,7 @@ impl DirkManager { }) .await; - return response - .is_ok_and(|res| res.into_inner().state() == ResponseState::Succeeded); + response.is_ok_and(|res| res.into_inner().state() == ResponseState::Succeeded) }; requests.push(request); @@ -528,7 +524,7 @@ impl DirkManager { let responses = join_all(requests).await; match account { Account::Simple(_) => { - if responses.get(0).is_some_and(|x| *x) { + if responses.first().is_some_and(|x| *x) { Ok(()) } else { bail!("Failed to unlock account") From 0a4ed8f0d6b69a857842326344c47e0ca3731f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 5 Mar 2025 18:16:23 -0300 Subject: [PATCH 098/104] remove unwraps --- crates/signer/src/manager/dirk.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 12a332e7..1404e154 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -671,18 +671,14 @@ fn load_distributed_accounts( fn aggregate_partial_signatures(partials: &[(BlsSignature, u32)]) -> eyre::Result { // Deserialize partial signatures into G2 points let mut shares: HashMap = HashMap::new(); - for (sig, id) in partials { - if sig.len() != BLS_SIGNATURE_BYTES_LEN { + for (signature, id) in partials { + if signature.len() != BLS_SIGNATURE_BYTES_LEN { bail!("Invalid signature length") } - let arr: [u8; BLS_SIGNATURE_BYTES_LEN] = (*sig).into(); - let opt: Option = G2Affine::from_compressed(&arr).into(); - let opt: Option = G2Projective::from(&opt.unwrap()).into(); - if let Some(point) = opt { - shares.insert(*id, point); - } else { - bail!("Failed to deserialize signature") - } + let affine = G2Affine::from_compressed(&signature) + .into_option() + .ok_or_eyre("Failed to deserialize signature")?; + shares.insert(*id, G2Projective::from(&affine)); } // Perform Lagrange interpolation to recover the master signature @@ -697,7 +693,11 @@ fn aggregate_partial_signatures(partials: &[(BlsSignature, u32)]) -> eyre::Resul denominator *= Scalar::from(*other_id) - Scalar::from(*id); } } - let lagrange_coeff = numerator * denominator.invert().unwrap(); + let lagrange_coeff = numerator * + denominator + .invert() + .into_option() + .ok_or_eyre("Failed to get lagrange coefficient")?; // Multiply the point by the Lagrange coefficient and add to the recovered point recovered += *point * lagrange_coeff; From 59c7abd7b4c76ead46fde812ce0e680bc1019c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 5 Mar 2025 18:24:37 -0300 Subject: [PATCH 099/104] Add function docs --- crates/signer/src/manager/dirk.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 1404e154..7f5fd303 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -80,10 +80,17 @@ struct ProxyAccount { #[derive(Clone, Debug)] pub struct DirkManager { + /// Chain config for the manager chain: Chain, + /// Consensus accounts available for signing. The key is the public key of + /// the account. consensus_accounts: HashMap, + /// Proxy accounts available for signing. The key is the public key of the + /// account. proxy_accounts: HashMap, + /// Path to store the passwords of the proxy accounts secrets_path: PathBuf, + /// Store to save proxy delegations delegations_store: Option, } @@ -163,6 +170,7 @@ impl DirkManager { }) } + /// Set the proxy store to use for storing proxy delegations pub fn with_proxy_store(self, store: ProxyStore) -> eyre::Result { if let ProxyStore::ERC2335 { .. } = store { return Err(eyre::eyre!("ERC2335 proxy store not supported")); @@ -171,14 +179,17 @@ impl DirkManager { Ok(Self { delegations_store: Some(store), ..self }) } + /// Get the number of available consensus signers pub fn available_consensus_signers(&self) -> usize { self.consensus_accounts.len() } + /// Get the number of available proxy signers pub fn available_proxy_signers(&self) -> usize { self.proxy_accounts.len() } + /// Get the map structure for `get_pubkey` endpoint pub fn get_consensus_proxy_maps(&self, module: &ModuleId) -> Vec { self.consensus_accounts .values() @@ -203,6 +214,7 @@ impl DirkManager { .collect() } + /// Request a signature from a consensus signer pub async fn request_consensus_signature( &self, pubkey: &BlsPublicKey, @@ -219,6 +231,7 @@ impl DirkManager { } } + /// Request a signature from a proxy signer pub async fn request_proxy_signature( &self, pubkey: &BlsPublicKey, @@ -235,6 +248,7 @@ impl DirkManager { } } + /// Sign a message with a `SimpleAccount` async fn request_simple_signature( &self, account: &SimpleAccount, @@ -264,6 +278,7 @@ impl DirkManager { }) } + /// Sign a message with a `DistributedAccount` async fn request_distributed_signature( &self, account: &DistributedAccount, @@ -331,6 +346,7 @@ impl DirkManager { .map_err(|e| SignerModuleError::Internal(e.to_string())) } + /// Generate a proxy key for a consensus signer pub async fn generate_proxy_key( &mut self, module: &ModuleId, @@ -365,6 +381,7 @@ impl DirkManager { Ok(delegation) } + /// Generate a proxy key for a `SimpleAccount` consensus signer async fn generate_simple_proxy_account( &self, consensus: &SimpleAccount, @@ -421,6 +438,7 @@ impl DirkManager { Ok(proxy_account) } + /// Generate a proxy key for a `DistributedAccount` consensus signer async fn generate_distributed_proxy_key( &self, consensus: &DistributedAccount, @@ -486,6 +504,7 @@ impl DirkManager { )) } + /// Store the password for a proxy account in disk fn store_password(&self, account: &ProxyAccount, password: String) -> eyre::Result<()> { let full_name = account.inner.full_name(); let (parent, name) = full_name.rsplit_once('/').ok_or_eyre("Invalid account name")?; @@ -498,6 +517,7 @@ impl DirkManager { Ok(()) } + /// Unlock an account in Dirk async fn unlock_account(&self, account: &Account, password: String) -> eyre::Result<()> { let participants = match account { Account::Simple(account) => vec![&account.connection], @@ -541,6 +561,7 @@ impl DirkManager { } } +/// Connect to a Dirk host async fn connect( server: &DirkHostConfig, client: &Identity, @@ -563,6 +584,7 @@ async fn connect( .map_err(eyre::Error::from) } +/// Decompose a full account name into wallet and name fn decompose_name(full_name: &str) -> eyre::Result<(String, String)> { full_name .split_once('/') @@ -570,6 +592,7 @@ fn decompose_name(full_name: &str) -> eyre::Result<(String, String)> { .ok_or_else(|| eyre::eyre!("Invalid account name")) } +/// Load `SimpleAccount`s into the consensus accounts map fn load_simple_accounts( accounts: Vec, host: &DirkHostConfig, @@ -606,6 +629,7 @@ fn load_simple_accounts( } } +/// Load `DistributedAccount`s into the consensus accounts map fn load_distributed_accounts( accounts: Vec, host: &DirkHostConfig, @@ -668,6 +692,7 @@ fn load_distributed_accounts( Ok(()) } +/// Aggregate partial signatures into a master signature fn aggregate_partial_signatures(partials: &[(BlsSignature, u32)]) -> eyre::Result { // Deserialize partial signatures into G2 points let mut shares: HashMap = HashMap::new(); From 1399de4e3371da6d00f9165e19457b6cd48a4019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 5 Mar 2025 18:54:30 -0300 Subject: [PATCH 100/104] Fix clippy --- crates/signer/src/manager/dirk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 7f5fd303..bbb2f641 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -700,7 +700,7 @@ fn aggregate_partial_signatures(partials: &[(BlsSignature, u32)]) -> eyre::Resul if signature.len() != BLS_SIGNATURE_BYTES_LEN { bail!("Invalid signature length") } - let affine = G2Affine::from_compressed(&signature) + let affine = G2Affine::from_compressed(signature) .into_option() .ok_or_eyre("Failed to deserialize signature")?; shares.insert(*id, G2Projective::from(&affine)); From 734e3236c4e9e529e41f283286657e9d0485eb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 12 Mar 2025 16:34:51 -0300 Subject: [PATCH 101/104] Add prefix to object_root logs --- crates/common/src/commit/request.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index eaeeb37d..36b57cd7 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -85,19 +85,19 @@ impl Display for SignRequest { f, "Consensus(pubkey: {}, object_root: {})", req.pubkey, - hex::encode(req.object_root) + hex::encode_prefixed(req.object_root) ), SignRequest::ProxyBls(req) => write!( f, "BLS(pubkey: {}, object_root: {})", req.pubkey, - hex::encode(req.object_root) + hex::encode_prefixed(req.object_root) ), SignRequest::ProxyEcdsa(req) => write!( f, "ECDSA(pubkey: {}, object_root: {})", req.pubkey, - hex::encode(req.object_root) + hex::encode_prefixed(req.object_root) ), } } From b7d5052c68ed1fe756f0b0f4a153cf27ebfebf35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 12 Mar 2025 16:48:23 -0300 Subject: [PATCH 102/104] Move signer module check --- crates/cli/src/docker_init.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index e50bb61b..538e05e9 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -318,12 +318,12 @@ pub async fn handle_docker_init(config_path: String, output_dir: String) -> Resu services.insert("cb_pbs".to_owned(), Some(pbs_service)); - let Some(signer_config) = cb_config.signer else { - panic!("Signer module required but no signer config provided"); - }; - // setup signer service if needs_signer_module { + let Some(signer_config) = cb_config.signer else { + panic!("Signer module required but no signer config provided"); + }; + match signer_config.inner { SignerType::Local { loader, store } => { if metrics_enabled { From c6504d284c6bace810b646d3f79b83ebd809920c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Thu, 13 Mar 2025 13:52:36 -0300 Subject: [PATCH 103/104] Add unit test for signature aggregation --- crates/signer/src/manager/dirk.rs | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index bbb2f641..ca39df0d 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -738,3 +738,37 @@ fn random_password() -> String { let password_bytes: [u8; 32] = rand::thread_rng().gen(); hex::encode(password_bytes) } + +mod test { + + #[test] + fn test_signature_aggregation() { + use alloy::hex; + use cb_common::signer::BlsSignature; + + use super::aggregate_partial_signatures; + + let partials = vec![ + (BlsSignature::from_slice(&hex::decode("aa16233b9e65b596caf070122d564ad7a021dad4fc2ed8508fccecfab010da80892fad7336e9fbada607c50e2d0d78e00c9961f26618334ec9f0e7ea225212f3c0c7d66f73ff1c2e555712a3e31f517b8329bd0ad9e15a9aeaa91521ba83502c").unwrap()), 1), + (BlsSignature::from_slice(&hex::decode("b27dd4c088e386edc4d07b6b23c72ba87a34e04cffd4975e8cb679aa4640cec1d34ace3e2bf33ac0dffca023c82422840012bb6c92eab36ca7908a9f9519fa18b1ed2bdbc624a98e01ca217c318a021495cc6cc9c8b982d0afed2cd83dc8fe65").unwrap()), 2), + (BlsSignature::from_slice(&hex::decode("aca4a71373df2f76369e8b242b0e2b1f41fc384feee3abe605ee8d6723f5fb11de1c9bd2408f4a09be981342352c523801e3beea73893a329204dd67fe84cb520220af33f7fa027b6bcc3b7c8e78647f2aa372145e4d3aec7682d2605040a64a").unwrap()), 3) + ]; + let expected = BlsSignature::from_slice(&hex::decode("0x8e343f074f91d19fd5118d9301768e30cecb21fb96a1ad9539cbdeae8907e2e12a88c91fe1d7e1f6995dcde18fb0272b1512cd68800e14ebd1c7f189e7221ba238a0f196226385737157f4b72d348c1886ce18d0a9609ba0cd5503e41546286f").unwrap()); + + // With all signers + let signature = aggregate_partial_signatures(&partials).unwrap(); + assert_eq!(signature, expected); + + // With only 2 signers + let signature = aggregate_partial_signatures(&partials[..2]).unwrap(); + assert_eq!(signature, expected); + + // With other 2 signers + let signature = aggregate_partial_signatures(&partials[1..]).unwrap(); + assert_eq!(signature, expected); + + // Should fail with only 1 signer + let signature = aggregate_partial_signatures(&partials[..1]).unwrap(); + assert_ne!(signature, expected); + } +} From d8ec4edbf22c0bdac5fe8dde87a3929d0e4287b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 14 Mar 2025 12:25:10 -0300 Subject: [PATCH 104/104] Update docs --- docs/docs/developing/environment-setup.md | 10 ++++++++++ docs/docs/get_started/configuration.md | 1 - docs/sidebars.js | 1 + examples/configs/dirk_signer.toml | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/docs/developing/environment-setup.md diff --git a/docs/docs/developing/environment-setup.md b/docs/docs/developing/environment-setup.md new file mode 100644 index 00000000..048b2df9 --- /dev/null +++ b/docs/docs/developing/environment-setup.md @@ -0,0 +1,10 @@ +--- +sidebar_position: 3 +--- +# Environment setup + +## Dirk signer + +In order to test Commit-Boost with a Dirk signer, you need to have a running Dirk instance. You can find a complete step-by-step guide on how to setup one in the Dirk's docs [here](https://github.com/attestantio/dirk/blob/master/docs/distributed_key_generation.md). + +If you are using a custom certificate authority, don't forget to add the CA certificate to the TOML config under `signer.dirk.ca_cert_path`. diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 37547611..7f432bf5 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -251,7 +251,6 @@ key_path = "/path/to/client.key" secrets_path = "/path/to/secrets" # Optional parameters ca_cert_path = "/path/to/ca.crt" -server_domain = "server.example.com" # Add one entry like this for each host [[signer.dirk.hosts]] diff --git a/docs/sidebars.js b/docs/sidebars.js index 4ba884f0..94725c02 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -54,6 +54,7 @@ const sidebars = { items: [ 'developing/custom-modules', 'developing/commit-module', + 'developing/environment-setup', ], }, { diff --git a/examples/configs/dirk_signer.toml b/examples/configs/dirk_signer.toml index cc2c5045..5dd1c9d3 100644 --- a/examples/configs/dirk_signer.toml +++ b/examples/configs/dirk_signer.toml @@ -13,6 +13,7 @@ docker_image = "commitboost_signer" cert_path = "/path/to/client.crt" key_path = "/path/to/client.key" secrets_path = "./dirk_secrets" +ca_cert_path = "/path/to/ca.crt" # Example of a single Dirk host [[signer.dirk.hosts]]