From f4330f732ac6c3f04da1d0a56538939baba0d15e Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Wed, 16 Oct 2024 16:58:46 +0800 Subject: [PATCH] fix(f3): handle jwt properly in f3 sidecar and set proper permissions for F3.* RPC methods (#4902) --- f3-sidecar/README.md | 5 +++-- f3-sidecar/ec.go | 26 ++++++++++++++++++++------ f3-sidecar/ec_test.go | 4 ++-- f3-sidecar/ffi_gen.go | 6 +++--- f3-sidecar/ffi_impl.go | 4 ++-- f3-sidecar/main.go | 4 +++- f3-sidecar/run.go | 40 +++++++++++++++++++++++----------------- src/daemon/mod.rs | 9 +++++---- src/f3/go_ffi.rs | 1 + src/f3/mod.rs | 2 ++ src/rpc/methods/f3.rs | 4 ++-- 11 files changed, 66 insertions(+), 39 deletions(-) diff --git a/f3-sidecar/README.md b/f3-sidecar/README.md index 351ff880258..99d40421919 100644 --- a/f3-sidecar/README.md +++ b/f3-sidecar/README.md @@ -13,10 +13,11 @@ Follow https://go.dev/doc/install or use one of the version managers of Go. ### Run sidecar -- run a forest node on calibnet +- run a forest node on calibnet (make sure to save jwt token with + `--save-token jwt_path`) - import a shared miner key for testing `forest-wallet --remote-wallet import` (the shared miner worker key can be found in `scripts/tests/api_compare/.env`) -- run f3 sidecar `go run .` +- run f3 sidecar `go run . -jwt $(cat jwt_path)` - (optional) to inspect RPC calls, run `mitmproxy --mode reverse:http://localhost:2345 --listen-port 8080` then `go run . -rpc http://127.0.0.1:8080/rpc/v1` diff --git a/f3-sidecar/ec.go b/f3-sidecar/ec.go index 42a87626346..7050a2aadc1 100644 --- a/f3-sidecar/ec.go +++ b/f3-sidecar/ec.go @@ -2,6 +2,8 @@ package main import ( "context" + "fmt" + "net/http" "github.com/filecoin-project/go-f3/ec" "github.com/filecoin-project/go-f3/gpbft" @@ -9,18 +11,24 @@ import ( ) type ForestEC struct { - rpcEndpoint string - f3api F3Api - closer jsonrpc.ClientCloser + rpcEndpoint string + isJwtProvided bool + f3api F3Api + closer jsonrpc.ClientCloser } -func NewForestEC(rpcEndpoint string) (ForestEC, error) { +func NewForestEC(rpcEndpoint, jwt string) (ForestEC, error) { f3api := F3Api{} - closer, err := jsonrpc.NewClient(context.Background(), rpcEndpoint, "F3", &f3api, nil) + headers := make(http.Header) + isJwtProvided := len(jwt) > 0 + if isJwtProvided { + headers.Add("Authorization", fmt.Sprintf("Bearer %s", jwt)) + } + closer, err := jsonrpc.NewClient(context.Background(), rpcEndpoint, "F3", &f3api, headers) if err != nil { return ForestEC{}, err } - return ForestEC{rpcEndpoint, f3api, closer}, nil + return ForestEC{rpcEndpoint, isJwtProvided, f3api, closer}, nil } func (ec *ForestEC) Close() { @@ -48,10 +56,16 @@ func (ec *ForestEC) GetPowerTable(ctx context.Context, tsk gpbft.TipSetKey) (gpb } func (ec *ForestEC) Finalize(ctx context.Context, tsk gpbft.TipSetKey) error { + if !ec.isJwtProvided { + return fmt.Errorf("unable to finalize tipset, jwt is not provided") + } return ec.f3api.Finalize(ctx, tsk) } func (ec *ForestEC) Sign(ctx context.Context, sender gpbft.PubKey, msg []byte) ([]byte, error) { + if !ec.isJwtProvided { + return nil, fmt.Errorf("unable to sign messages, jwt is not provided") + } signature, err := ec.f3api.SignMessage(ctx, sender, msg) if err != nil { return nil, err diff --git a/f3-sidecar/ec_test.go b/f3-sidecar/ec_test.go index 571267ab4a3..436199d7eca 100644 --- a/f3-sidecar/ec_test.go +++ b/f3-sidecar/ec_test.go @@ -16,7 +16,7 @@ var ( ) func init() { - ec, err := NewForestEC("http://127.0.0.1:2345/rpc/v1") + ec, err := NewForestEC("http://127.0.0.1:2345/rpc/v1", "") if err != nil { panic(err) } @@ -69,5 +69,5 @@ func TestFinalize(t *testing.T) { head, err := EC.GetHead(ctx) require.NoError(t, err) err = EC.Finalize(ctx, head.Key()) - require.NoError(t, err) + require.ErrorContains(t, err, "unable to finalize tipset, jwt is not provided") } diff --git a/f3-sidecar/ffi_gen.go b/f3-sidecar/ffi_gen.go index fe0a2ae2aee..3ed6b825bc5 100644 --- a/f3-sidecar/ffi_gen.go +++ b/f3-sidecar/ffi_gen.go @@ -33,12 +33,12 @@ import ( var GoF3NodeImpl GoF3Node type GoF3Node interface { - run(rpc_endpoint string, f3_rpc_endpoint string, initial_power_table string, bootstrap_epoch int64, finality int64, f3_root string, manifest_server string) bool + run(rpc_endpoint string, jwt string, f3_rpc_endpoint string, initial_power_table string, bootstrap_epoch int64, finality int64, f3_root string, manifest_server string) bool } //export CGoF3Node_run -func CGoF3Node_run(rpc_endpoint C.StringRef, f3_rpc_endpoint C.StringRef, initial_power_table C.StringRef, bootstrap_epoch C.int64_t, finality C.int64_t, f3_root C.StringRef, manifest_server C.StringRef, slot *C.void, cb *C.void) { - resp := GoF3NodeImpl.run(newString(rpc_endpoint), newString(f3_rpc_endpoint), newString(initial_power_table), newC_int64_t(bootstrap_epoch), newC_int64_t(finality), newString(f3_root), newString(manifest_server)) +func CGoF3Node_run(rpc_endpoint C.StringRef, jwt C.StringRef, f3_rpc_endpoint C.StringRef, initial_power_table C.StringRef, bootstrap_epoch C.int64_t, finality C.int64_t, f3_root C.StringRef, manifest_server C.StringRef, slot *C.void, cb *C.void) { + resp := GoF3NodeImpl.run(newString(rpc_endpoint), newString(jwt), newString(f3_rpc_endpoint), newString(initial_power_table), newC_int64_t(bootstrap_epoch), newC_int64_t(finality), newString(f3_root), newString(manifest_server)) resp_ref, buffer := cvt_ref(cntC_bool, refC_bool)(&resp) C.GoF3Node_run_cb(unsafe.Pointer(cb), resp_ref, unsafe.Pointer(slot)) runtime.KeepAlive(resp) diff --git a/f3-sidecar/ffi_impl.go b/f3-sidecar/ffi_impl.go index 852ab99bae7..b687fcf1f55 100644 --- a/f3-sidecar/ffi_impl.go +++ b/f3-sidecar/ffi_impl.go @@ -25,8 +25,8 @@ type f3Impl struct { ctx context.Context } -func (f3 *f3Impl) run(rpc_endpoint string, f3_rpc_endpoint string, initial_power_table string, bootstrap_epoch int64, finality int64, db string, manifest_server string) bool { - err := run(f3.ctx, rpc_endpoint, f3_rpc_endpoint, initial_power_table, bootstrap_epoch, finality, db, manifest_server) +func (f3 *f3Impl) run(rpc_endpoint string, jwt string, f3_rpc_endpoint string, initial_power_table string, bootstrap_epoch int64, finality int64, db string, manifest_server string) bool { + err := run(f3.ctx, rpc_endpoint, jwt, f3_rpc_endpoint, initial_power_table, bootstrap_epoch, finality, db, manifest_server) return err == nil } diff --git a/f3-sidecar/main.go b/f3-sidecar/main.go index dfe361404ea..927349a61bd 100644 --- a/f3-sidecar/main.go +++ b/f3-sidecar/main.go @@ -26,6 +26,8 @@ func main() { var rpcEndpoint string flag.StringVar(&rpcEndpoint, "rpc", "http://127.0.0.1:2345/rpc/v1", "forest RPC endpoint") + var jwt string + flag.StringVar(&jwt, "jwt", "", "the JWT token for invoking forest RPC methods that require WRITE and SIGN permission") var f3RpcEndpoint string flag.StringVar(&f3RpcEndpoint, "f3-rpc", "127.0.0.1:23456", "The RPC endpoint F3 sidecar listens on") var initialPowerTable string @@ -42,7 +44,7 @@ func main() { ctx := context.Background() - err := run(ctx, rpcEndpoint, f3RpcEndpoint, initialPowerTable, bootstrapEpoch, finality, root, manifestServer) + err := run(ctx, rpcEndpoint, jwt, f3RpcEndpoint, initialPowerTable, bootstrapEpoch, finality, root, manifestServer) if err != nil { panic(err) } diff --git a/f3-sidecar/run.go b/f3-sidecar/run.go index 9c7f28b0e5b..196fe510671 100644 --- a/f3-sidecar/run.go +++ b/f3-sidecar/run.go @@ -21,8 +21,9 @@ import ( "github.com/libp2p/go-libp2p/core/peer" ) -func run(ctx context.Context, rpcEndpoint string, f3RpcEndpoint string, initialPowerTable string, bootstrapEpoch int64, finality int64, f3Root string, manifestServer string) error { +func run(ctx context.Context, rpcEndpoint string, jwt string, f3RpcEndpoint string, initialPowerTable string, bootstrapEpoch int64, finality int64, f3Root string, manifestServer string) error { api := FilecoinApi{} + isJwtProvided := len(jwt) > 0 closer, err := jsonrpc.NewClient(context.Background(), rpcEndpoint, "Filecoin", &api, nil) if err != nil { return err @@ -41,7 +42,7 @@ func run(ctx context.Context, rpcEndpoint string, f3RpcEndpoint string, initialP if err != nil { return err } - ec, err := NewForestEC(rpcEndpoint) + ec, err := NewForestEC(rpcEndpoint, jwt) if err != nil { return err } @@ -155,23 +156,28 @@ func run(ctx context.Context, rpcEndpoint string, f3RpcEndpoint string, initialP if err != nil { continue } - for _, miner := range miners { - signatureBuilder, err := msgToSign.PrepareSigningInputs(gpbft.ActorID(miner)) - if err != nil { - if errors.Is(err, gpbft.ErrNoPower) { - // we don't have any power in F3, continue - logger.Warnf("no power to participate in F3: %+v", err) - } else { - logger.Warnf("preparing signing inputs: %+v", err) + if !isJwtProvided && len(miners) > 0 { + logger.Warn("Unable to sign messages, jwt for Forest RPC endpoint is not provided.") + } + if isJwtProvided { + for _, miner := range miners { + signatureBuilder, err := msgToSign.PrepareSigningInputs(gpbft.ActorID(miner)) + if err != nil { + if errors.Is(err, gpbft.ErrNoPower) { + // we don't have any power in F3, continue + logger.Warnf("no power to participate in F3: %+v", err) + } else { + logger.Warnf("preparing signing inputs: %+v", err) + } + continue } - continue - } - payloadSig, vrfSig, err := signatureBuilder.Sign(ctx, &ec) - if err != nil { - logger.Warnf("signing message: %+v", err) + payloadSig, vrfSig, err := signatureBuilder.Sign(ctx, &ec) + if err != nil { + logger.Warnf("signing message: %+v", err) + } + logger.Debugf("miner with id %d is sending message in F3", miner) + f3Module.Broadcast(ctx, signatureBuilder, payloadSig, vrfSig) } - logger.Debugf("miner with id %d is sending message in F3", miner) - f3Module.Broadcast(ctx, signatureBuilder, payloadSig, vrfSig) } } } diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index b74777dc6a2..ab1cbbed2b5 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -168,7 +168,7 @@ pub(super) async fn start( keystore.put(JWT_IDENTIFIER, generate_priv_key())?; } - handle_admin_token(&opts, &keystore)?; + let admin_jwt = handle_admin_token(&opts, &keystore)?; let keystore = Arc::new(RwLock::new(keystore)); @@ -427,6 +427,7 @@ pub(super) async fn start( crate::f3::run_f3_sidecar_if_enabled( &chain_config, format!("http://{rpc_address}/rpc/v1"), + admin_jwt, crate::rpc::f3::get_f3_rpc_endpoint().to_string(), initial_power_table.to_string(), bootstrap_epoch, @@ -595,7 +596,7 @@ async fn set_snapshot_path_if_needed( /// Generates, prints and optionally writes to a file the administrator JWT /// token. -fn handle_admin_token(opts: &CliOpts, keystore: &KeyStore) -> anyhow::Result<()> { +fn handle_admin_token(opts: &CliOpts, keystore: &KeyStore) -> anyhow::Result { let ki = keystore.get(JWT_IDENTIFIER)?; // Lotus admin tokens do not expire but Forest requires all JWT tokens to // have an expiration date. So we set the expiration date to 100 years in @@ -608,10 +609,10 @@ fn handle_admin_token(opts: &CliOpts, keystore: &KeyStore) -> anyhow::Result<()> )?; info!("Admin token: {token}"); if let Some(path) = opts.save_token.as_ref() { - std::fs::write(path, token)?; + std::fs::write(path, &token)?; } - Ok(()) + Ok(token) } /// returns the first error with which any of the services end, or never returns at all diff --git a/src/f3/go_ffi.rs b/src/f3/go_ffi.rs index a0abc88b1f3..11aa708327d 100644 --- a/src/f3/go_ffi.rs +++ b/src/f3/go_ffi.rs @@ -11,6 +11,7 @@ pub mod binding { pub trait GoF3Node { fn run( rpc_endpoint: String, + jwt: String, f3_rpc_endpoint: String, initial_power_table: String, bootstrap_epoch: i64, diff --git a/src/f3/mod.rs b/src/f3/mod.rs index 35c16977bb5..3e4186e7dff 100644 --- a/src/f3/mod.rs +++ b/src/f3/mod.rs @@ -89,6 +89,7 @@ pub fn get_f3_sidecar_params(chain_config: &ChainConfig) -> F3Options { pub fn run_f3_sidecar_if_enabled( chain_config: &ChainConfig, _rpc_endpoint: String, + _jwt: String, _f3_rpc_endpoint: String, _initial_power_table: String, _bootstrap_epoch: i64, @@ -101,6 +102,7 @@ pub fn run_f3_sidecar_if_enabled( { GoF3NodeImpl::run( _rpc_endpoint, + _jwt, _f3_rpc_endpoint, _initial_power_table, _bootstrap_epoch, diff --git a/src/rpc/methods/f3.rs b/src/rpc/methods/f3.rs index 855afe8c386..f874a6324c4 100644 --- a/src/rpc/methods/f3.rs +++ b/src/rpc/methods/f3.rs @@ -459,7 +459,7 @@ impl RpcMethod<1> for Finalize { const NAME: &'static str = "F3.Finalize"; const PARAM_NAMES: [&'static str; 1] = ["tipset_key"]; const API_PATHS: ApiPaths = ApiPaths::V1; - const PERMISSION: Permission = Permission::Read; + const PERMISSION: Permission = Permission::Write; type Params = (F3TipSetKey,); type Ok = (); @@ -524,7 +524,7 @@ impl RpcMethod<2> for SignMessage { const NAME: &'static str = "F3.SignMessage"; const PARAM_NAMES: [&'static str; 2] = ["pubkey", "message"]; const API_PATHS: ApiPaths = ApiPaths::V1; - const PERMISSION: Permission = Permission::Read; + const PERMISSION: Permission = Permission::Sign; type Params = (Vec, Vec); type Ok = Signature;