diff --git a/api/api_common.go b/api/api_common.go index 73a0c984c..2a887a26a 100644 --- a/api/api_common.go +++ b/api/api_common.go @@ -3,6 +3,7 @@ package api import ( "context" "fmt" + "time" "github.com/google/uuid" @@ -49,6 +50,9 @@ type Common interface { // trigger graceful shutdown Shutdown(context.Context) error //perm:admin + // StartTime returns node start time + StartTime(context.Context) (time.Time, error) //perm:read + // Session returns a random UUID of api provider session Session(context.Context) (uuid.UUID, error) //perm:read diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index bfc4fc905..ab86d19cf 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -2272,6 +2272,21 @@ func (mr *MockFullNodeMockRecorder) Shutdown(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockFullNode)(nil).Shutdown), arg0) } +// StartTime mocks base method. +func (m *MockFullNode) StartTime(arg0 context.Context) (time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartTime", arg0) + ret0, _ := ret[0].(time.Time) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StartTime indicates an expected call of StartTime. +func (mr *MockFullNodeMockRecorder) StartTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartTime", reflect.TypeOf((*MockFullNode)(nil).StartTime), arg0) +} + // StateAccountKey mocks base method. func (m *MockFullNode) StateAccountKey(arg0 context.Context, arg1 address.Address, arg2 types.TipSetKey) (address.Address, error) { m.ctrl.T.Helper() diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 935eaca94..254d1274d 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -79,6 +79,8 @@ type CommonStruct struct { Shutdown func(p0 context.Context) error `perm:"admin"` + StartTime func(p0 context.Context) (time.Time, error) `perm:"read"` + Version func(p0 context.Context) (APIVersion, error) `perm:"read"` } } @@ -1162,6 +1164,17 @@ func (s *CommonStub) Shutdown(p0 context.Context) error { return ErrNotSupported } +func (s *CommonStruct) StartTime(p0 context.Context) (time.Time, error) { + if s.Internal.StartTime == nil { + return *new(time.Time), ErrNotSupported + } + return s.Internal.StartTime(p0) +} + +func (s *CommonStub) StartTime(p0 context.Context) (time.Time, error) { + return *new(time.Time), ErrNotSupported +} + func (s *CommonStruct) Version(p0 context.Context) (APIVersion, error) { if s.Internal.Version == nil { return *new(APIVersion), ErrNotSupported diff --git a/api/v0api/v0mocks/mock_full.go b/api/v0api/v0mocks/mock_full.go index 8df8a89f7..5cbcac8b5 100644 --- a/api/v0api/v0mocks/mock_full.go +++ b/api/v0api/v0mocks/mock_full.go @@ -2157,6 +2157,21 @@ func (mr *MockFullNodeMockRecorder) Shutdown(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockFullNode)(nil).Shutdown), arg0) } +// StartTime mocks base method. +func (m *MockFullNode) StartTime(arg0 context.Context) (time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartTime", arg0) + ret0, _ := ret[0].(time.Time) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StartTime indicates an expected call of StartTime. +func (mr *MockFullNodeMockRecorder) StartTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartTime", reflect.TypeOf((*MockFullNode)(nil).StartTime), arg0) +} + // StateAccountKey mocks base method. func (m *MockFullNode) StateAccountKey(arg0 context.Context, arg1 address.Address, arg2 types.TipSetKey) (address.Address, error) { m.ctrl.T.Helper() diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index 5e5c0e762..9db0b94b2 100644 Binary files a/build/openrpc/full.json.gz and b/build/openrpc/full.json.gz differ diff --git a/build/openrpc/gateway.json.gz b/build/openrpc/gateway.json.gz index 7ba78074e..6457d457f 100644 Binary files a/build/openrpc/gateway.json.gz and b/build/openrpc/gateway.json.gz differ diff --git a/build/openrpc/miner.json.gz b/build/openrpc/miner.json.gz index 8bf6e7c11..ae234e500 100644 Binary files a/build/openrpc/miner.json.gz and b/build/openrpc/miner.json.gz differ diff --git a/build/openrpc/worker.json.gz b/build/openrpc/worker.json.gz index 3db86b03c..054b285d4 100644 Binary files a/build/openrpc/worker.json.gz and b/build/openrpc/worker.json.gz differ diff --git a/cli/info.go b/cli/info.go index 0d036875a..007e3655f 100644 --- a/cli/info.go +++ b/cli/info.go @@ -41,7 +41,13 @@ func infoCmdAct(cctx *cli.Context) error { return err } + start, err := fullapi.StartTime(ctx) + if err != nil { + return err + } + fmt.Printf("Network: %s\n", network.NetworkName) + fmt.Printf("StartTime: %s (started at %s)\n", time.Now().Sub(start).Truncate(time.Second), start.Truncate(time.Second)) fmt.Print("Chain: ") err = SyncBasefeeCheck(ctx, fullapi) if err != nil { diff --git a/cmd/lotus-miner/info.go b/cmd/lotus-miner/info.go index 9065139eb..312d86600 100644 --- a/cmd/lotus-miner/info.go +++ b/cmd/lotus-miner/info.go @@ -92,6 +92,12 @@ func infoCmdAct(cctx *cli.Context) error { fmt.Println("Enabled subsystems (from markets API):", subsystems) + start, err := fullapi.StartTime(ctx) + if err != nil { + return err + } + fmt.Printf("StartTime: %s (started at %s)\n", time.Now().Sub(start).Truncate(time.Second), start.Truncate(time.Second)) + fmt.Print("Chain: ") err = lcli.SyncBasefeeCheck(ctx, fullapi) diff --git a/documentation/en/api-v0-methods-miner.md b/documentation/en/api-v0-methods-miner.md index 848600bc6..d999d7f22 100644 --- a/documentation/en/api-v0-methods-miner.md +++ b/documentation/en/api-v0-methods-miner.md @@ -167,6 +167,8 @@ * [SectorsSummary](#SectorsSummary) * [SectorsUnsealPiece](#SectorsUnsealPiece) * [SectorsUpdate](#SectorsUpdate) +* [Start](#Start) + * [StartTime](#StartTime) * [Storage](#Storage) * [StorageAddLocal](#StorageAddLocal) * [StorageAttach](#StorageAttach) @@ -3621,6 +3623,18 @@ Inputs: Response: `{}` +## Start + + +### StartTime + + +Perms: read + +Inputs: `null` + +Response: `"0001-01-01T00:00:00Z"` + ## Storage diff --git a/documentation/en/api-v0-methods.md b/documentation/en/api-v0-methods.md index 44a8ca554..5bcb25570 100644 --- a/documentation/en/api-v0-methods.md +++ b/documentation/en/api-v0-methods.md @@ -156,6 +156,8 @@ * [PaychVoucherCreate](#PaychVoucherCreate) * [PaychVoucherList](#PaychVoucherList) * [PaychVoucherSubmit](#PaychVoucherSubmit) +* [Start](#Start) + * [StartTime](#StartTime) * [State](#State) * [StateAccountKey](#StateAccountKey) * [StateActorCodeCIDs](#StateActorCodeCIDs) @@ -4615,6 +4617,18 @@ Response: } ``` +## Start + + +### StartTime + + +Perms: read + +Inputs: `null` + +Response: `"0001-01-01T00:00:00Z"` + ## State The State methods are used to query, inspect, and interact with chain state. Most methods take a TipSetKey as a parameter. The state looked up is the parent state of the tipset. diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index ff360e16a..feca555a7 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -164,6 +164,8 @@ * [PaychVoucherCreate](#PaychVoucherCreate) * [PaychVoucherList](#PaychVoucherList) * [PaychVoucherSubmit](#PaychVoucherSubmit) +* [Start](#Start) + * [StartTime](#StartTime) * [State](#State) * [StateAccountKey](#StateAccountKey) * [StateActorCodeCIDs](#StateActorCodeCIDs) @@ -5047,6 +5049,18 @@ Response: } ``` +## Start + + +### StartTime + + +Perms: read + +Inputs: `null` + +Response: `"0001-01-01T00:00:00Z"` + ## State The State methods are used to query, inspect, and interact with chain state. Most methods take a TipSetKey as a parameter. The state looked up is the parent state of the tipset. diff --git a/node/builder.go b/node/builder.go index ca31a2c91..9690b7f7c 100644 --- a/node/builder.go +++ b/node/builder.go @@ -155,6 +155,7 @@ func defaults() []Option { Override(new(journal.DisabledEvents), journal.EnvDisabledEvents), Override(new(journal.Journal), modules.OpenFilesystemJournal), Override(new(*alerting.Alerting), alerting.NewAlertingSystem), + Override(new(dtypes.NodeStartTime), FromVal(dtypes.NodeStartTime(time.Now()))), Override(CheckFDLimit, modules.CheckFdLimit(build.DefaultFDLimit)), diff --git a/node/impl/common/common.go b/node/impl/common/common.go index 182b74989..eff6b58b8 100644 --- a/node/impl/common/common.go +++ b/node/impl/common/common.go @@ -2,6 +2,7 @@ package common import ( "context" + "time" "github.com/gbrlsnchs/jwt/v3" "github.com/google/uuid" @@ -26,6 +27,8 @@ type CommonAPI struct { Alerting *alerting.Alerting APISecret *dtypes.APIAlg ShutdownChan dtypes.ShutdownChan + + Start dtypes.NodeStartTime } type jwtPayload struct { @@ -91,3 +94,7 @@ func (a *CommonAPI) Session(ctx context.Context) (uuid.UUID, error) { func (a *CommonAPI) Closing(ctx context.Context) (<-chan struct{}, error) { return make(chan struct{}), nil // relies on jsonrpc closing } + +func (a *CommonAPI) StartTime(context.Context) (time.Time, error) { + return time.Time(a.Start), nil +} diff --git a/node/modules/dtypes/api.go b/node/modules/dtypes/api.go index d57b05cfa..6de511b9d 100644 --- a/node/modules/dtypes/api.go +++ b/node/modules/dtypes/api.go @@ -1,6 +1,8 @@ package dtypes import ( + "time" + "github.com/gbrlsnchs/jwt/v3" "github.com/multiformats/go-multiaddr" ) @@ -8,3 +10,5 @@ import ( type APIAlg jwt.HMACSHA type APIEndpoint multiaddr.Multiaddr + +type NodeStartTime time.Time diff --git a/node/options.go b/node/options.go index 0793a150f..26c2c247f 100644 --- a/node/options.go +++ b/node/options.go @@ -93,6 +93,12 @@ func From(typ interface{}) interface{} { }).Interface() } +func FromVal[T any](v T) func() T { + return func() T { + return v + } +} + // from go-ipfs // as casts input constructor to a given interface (if a value is given, it // wraps it into a constructor).