From d5bcb7d5a6026887153538a549c34e29beed693e Mon Sep 17 00:00:00 2001 From: esuwu Date: Tue, 11 Nov 2025 10:23:12 +0100 Subject: [PATCH 01/17] Drafted endpoints for API --- cmd/node/node.go | 2 +- pkg/api/app.go | 11 +++- pkg/api/app_test.go | 8 ++- pkg/api/blocks_test.go | 15 ++++- pkg/api/node_api.go | 100 ++++++++++++++++++++++++++++++++ pkg/api/node_api_test.go | 11 +++- pkg/api/peers_test.go | 24 ++++++-- pkg/api/routes.go | 10 ++++ pkg/mock/state.go | 90 ++++++++++++++++++++++++++++ pkg/state/api.go | 3 + pkg/state/state.go | 15 +++++ pkg/state/threadsafe_wrapper.go | 18 ++++++ 12 files changed, 294 insertions(+), 13 deletions(-) diff --git a/cmd/node/node.go b/cmd/node/node.go index b7f213071d..c8896f3efd 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -460,7 +460,7 @@ func runNode(ctx context.Context, nc *config) (_ io.Closer, retErr error) { return nil, errors.Wrap(err, "failed to create services") } - app, err := api.NewApp(nc.apiKey, minerScheduler, svs) + app, err := api.NewApp(nc.apiKey, minerScheduler, svs, cfg) if err != nil { return nil, errors.Wrap(err, "failed to initialize application") } diff --git a/pkg/api/app.go b/pkg/api/app.go index af88798821..1714e35573 100644 --- a/pkg/api/app.go +++ b/pkg/api/app.go @@ -13,6 +13,7 @@ import ( "github.com/wavesplatform/gowaves/pkg/node/peers" "github.com/wavesplatform/gowaves/pkg/proto" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" "github.com/wavesplatform/gowaves/pkg/state" "github.com/wavesplatform/gowaves/pkg/types" ) @@ -35,6 +36,7 @@ const ( type appSettings struct { BlockRequestLimit uint64 AssetDetailsLimit int + GenerationPeriod uint64 } func defaultAppSettings() *appSettings { @@ -56,11 +58,13 @@ type App struct { settings *appSettings } -func NewApp(apiKey string, scheduler SchedulerEmits, services services.Services) (*App, error) { - return newApp(apiKey, scheduler, services, nil) +func NewApp(apiKey string, scheduler SchedulerEmits, services services.Services, + cfg *settings.BlockchainSettings) (*App, error) { + return newApp(apiKey, scheduler, services, nil, cfg) } -func newApp(apiKey string, scheduler SchedulerEmits, services services.Services, settings *appSettings) (*App, error) { +func newApp(apiKey string, scheduler SchedulerEmits, services services.Services, settings *appSettings, + cfg *settings.BlockchainSettings) (*App, error) { if settings == nil { settings = defaultAppSettings() } @@ -69,6 +73,7 @@ func newApp(apiKey string, scheduler SchedulerEmits, services services.Services, return nil, err } + settings.GenerationPeriod = cfg.GenerationPeriod return &App{ hashedApiKey: digest, apiKeyEnabled: len(apiKey) > 0, diff --git a/pkg/api/app_test.go b/pkg/api/app_test.go index fdec4901b8..54fc58b1b1 100644 --- a/pkg/api/app_test.go +++ b/pkg/api/app_test.go @@ -5,10 +5,16 @@ import ( "github.com/stretchr/testify/require" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" ) func TestAppAuth(t *testing.T) { - app, _ := NewApp("apiKey", nil, services.Services{}) + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + app, _ := NewApp("apiKey", nil, services.Services{}, cfg) require.Error(t, app.checkAuth("bla")) require.NoError(t, app.checkAuth("apiKey")) } diff --git a/pkg/api/blocks_test.go b/pkg/api/blocks_test.go index badb0212aa..865ec87691 100644 --- a/pkg/api/blocks_test.go +++ b/pkg/api/blocks_test.go @@ -11,6 +11,7 @@ import ( "github.com/wavesplatform/gowaves/pkg/mock" "github.com/wavesplatform/gowaves/pkg/proto" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" ) func TestApp_BlocksFirst(t *testing.T) { @@ -26,7 +27,12 @@ func TestApp_BlocksFirst(t *testing.T) { s := mock.NewMockState(ctrl) s.EXPECT().BlockByHeight(proto.Height(1)).Return(g, nil) - app, err := NewApp("api-key", nil, services.Services{State: s}) + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + app, err := NewApp("api-key", nil, services.Services{State: s}, cfg) require.NoError(t, err) first, err := app.BlocksFirst() require.NoError(t, err) @@ -42,7 +48,12 @@ func TestApp_BlocksLast(t *testing.T) { s.EXPECT().Height().Return(proto.Height(1), nil) s.EXPECT().BlockByHeight(proto.Height(1)).Return(g, nil) - app, err := NewApp("api-key", nil, services.Services{State: s}) + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + app, err := NewApp("api-key", nil, services.Services{State: s}, cfg) require.NoError(t, err) first, err := app.BlocksLast() require.NoError(t, err) diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index 37655afe99..d0bd6a5329 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -21,6 +21,7 @@ import ( "github.com/wavesplatform/gowaves/pkg/errs" "github.com/wavesplatform/gowaves/pkg/logging" "github.com/wavesplatform/gowaves/pkg/proto" + "github.com/wavesplatform/gowaves/pkg/settings" "github.com/wavesplatform/gowaves/pkg/state" "github.com/wavesplatform/gowaves/pkg/state/stateerr" "github.com/wavesplatform/gowaves/pkg/util/limit_listener" @@ -895,6 +896,105 @@ func (a *NodeApi) snapshotStateHash(w http.ResponseWriter, r *http.Request) erro return nil } +type GeneratorInfo struct { + Address string `json:"address"` + Balance uint64 `json:"balance"` + TransactionID string `json:"transactionID"` +} + +func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { + heightStr := chi.URLParam(r, "height") + height, err := strconv.ParseUint(heightStr, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid height") + } + + activationHeight, err := a.state.ActivationHeight(int16(settings.DeterministicFinality)) + if err != nil { + return fmt.Errorf("failed to get DeterministicFinality activation height: %w", err) + } + + periodStart, err := state.CurrentGenerationPeriodStart(activationHeight, height, + a.app.settings.GenerationPeriod) + if err != nil { + return err + } + + var generatorsInfo []GeneratorInfo + + generatorAddresses, err := a.state.CommittedGenerators(periodStart) + if err != nil { + return err + } + for _, generatorAddress := range generatorAddresses { + endorserRecipient := proto.NewRecipientFromAddress(generatorAddress) + balance, pullErr := a.state.GeneratingBalance(endorserRecipient, height) + if pullErr != nil { + return pullErr + } + generatorsInfo = append(generatorsInfo, GeneratorInfo{ + Address: generatorAddress.String(), + Balance: balance, + TransactionID: "", // TODO should be somehow found. + }) + } + + return trySendJSON(w, generatorsInfo) +} + +func (a *NodeApi) FinalizedHeight(w http.ResponseWriter, _ *http.Request) error { + h, err := a.state.LastFinalizedHeight() + if err != nil { + return err + } + return trySendJSON(w, map[string]uint64{"height": h}) +} + +func (a *NodeApi) FinalizedHeader(w http.ResponseWriter, _ *http.Request) error { + block, err := a.app.state.LastFinalizedBlock() + if err != nil { + return err + } + blockHeader := proto.BlockHeader{ + Version: block.Version, + Timestamp: block.Timestamp, + Parent: block.Parent, + FeaturesCount: block.FeaturesCount, + Features: block.Features, + RewardVote: block.RewardVote, + ConsensusBlockLength: block.ConsensusBlockLength, + NxtConsensus: block.NxtConsensus, + TransactionBlockLength: block.TransactionBlockLength, + TransactionCount: block.TransactionCount, + GeneratorPublicKey: block.GeneratorPublicKey, + BlockSignature: block.BlockSignature, + TransactionsRoot: block.TransactionsRoot, + StateHash: block.StateHash, + ChallengedHeader: block.ChallengedHeader, + FinalizationVoting: block.FinalizationVoting, + ID: block.ID, + } + return trySendJSON(w, blockHeader) +} + +func (a *NodeApi) FinalizedHeightAt(w http.ResponseWriter, r *http.Request) error { + heightStr := chi.URLParam(r, "height") + height, err := strconv.ParseUint(heightStr, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid height") + } + + h, err := a.state.FinalizedHeightAt(height) + if err != nil { + return err + } + return trySendJSON(w, map[string]uint64{"height": h}) +} + +func (a *NodeApi) TransactionsSignCommit(_ http.ResponseWriter, _ *http.Request) error { + return nil +} + func wavesAddressInvalidCharErr(invalidChar rune, id string) *apiErrs.CustomValidationError { return apiErrs.NewCustomValidationError( fmt.Sprintf( diff --git a/pkg/api/node_api_test.go b/pkg/api/node_api_test.go index ce9b55c2c3..998318730f 100644 --- a/pkg/api/node_api_test.go +++ b/pkg/api/node_api_test.go @@ -17,6 +17,7 @@ import ( "github.com/wavesplatform/gowaves/pkg/mock" "github.com/wavesplatform/gowaves/pkg/proto" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" ) const apiKey = "X-API-Key" @@ -75,6 +76,12 @@ func TestNodeApi_WavesRegularBalanceByAddress(t *testing.T) { return req } + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + t.Run("success", func(t *testing.T) { const ( addrStr = "3Myqjf1D44wR8Vko4Tr5CwSzRNo2Vg9S7u7" @@ -93,7 +100,7 @@ func TestNodeApi_WavesRegularBalanceByAddress(t *testing.T) { a, err := NewApp("", nil, services.Services{ State: st, Scheme: proto.TestNetScheme, - }) + }, cfg) require.NoError(t, err) aErr := NewNodeAPI(a, nil).WavesRegularBalanceByAddress(resp, req) @@ -120,7 +127,7 @@ func TestNodeApi_WavesRegularBalanceByAddress(t *testing.T) { a, err := NewApp("", nil, services.Services{ State: mock.NewMockState(ctrl), Scheme: proto.TestNetScheme, - }) + }, cfg) require.NoError(t, err) aErr := NewNodeAPI(a, nil).WavesRegularBalanceByAddress(resp, req) diff --git a/pkg/api/peers_test.go b/pkg/api/peers_test.go index b8a775f0ff..38496c4bd5 100644 --- a/pkg/api/peers_test.go +++ b/pkg/api/peers_test.go @@ -15,9 +15,16 @@ import ( "github.com/wavesplatform/gowaves/pkg/mock" "github.com/wavesplatform/gowaves/pkg/services" + "github.com/wavesplatform/gowaves/pkg/settings" ) func TestApp_PeersKnown(t *testing.T) { + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -25,7 +32,7 @@ func TestApp_PeersKnown(t *testing.T) { addr := proto.NewTCPAddr(net.ParseIP("127.0.0.1"), 6868).ToIpPort() peerManager.EXPECT().KnownPeers().Return([]storage.KnownPeer{storage.KnownPeer(addr)}) - app, err := NewApp("key", nil, services.Services{Peers: peerManager}) + app, err := NewApp("key", nil, services.Services{Peers: peerManager}, cfg) require.NoError(t, err) rs2, err := app.PeersKnown() @@ -40,6 +47,11 @@ func TestApp_PeersSuspended(t *testing.T) { peerManager := mock.NewMockPeerManager(ctrl) now := time.Now() + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } ips := []string{"13.3.4.1", "5.3.6.7"} testData := []storage.SuspendedPeer{ @@ -59,7 +71,7 @@ func TestApp_PeersSuspended(t *testing.T) { peerManager.EXPECT().Suspended().Return(testData) - app, err := NewApp("key", nil, services.Services{Peers: peerManager}) + app, err := NewApp("key", nil, services.Services{Peers: peerManager}, cfg) require.NoError(t, err) suspended := app.PeersSuspended() @@ -100,8 +112,12 @@ func TestApp_PeersBlackList(t *testing.T) { } peerManager.EXPECT().BlackList().Return(testData) - - app, err := NewApp("key", nil, services.Services{Peers: peerManager}) + cfg := &settings.BlockchainSettings{ + FunctionalitySettings: settings.FunctionalitySettings{ + GenerationPeriod: 0, + }, + } + app, err := NewApp("key", nil, services.Services{Peers: peerManager}, cfg) require.NoError(t, err) blackList := app.PeersBlackListed() diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 218c7e2db1..176bcf0140 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -107,6 +107,10 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { r.Get("/height/{id}", wrapper(a.BlockHeightByID)) r.Get("/at/{height}", wrapper(a.BlockAt)) r.Get("/{id}", wrapper(a.BlockIDAt)) + // Finalization. + r.Get("/height/finalized", wrapper(a.FinalizedHeight)) + r.Get("/headers/finalized", wrapper(a.FinalizedHeader)) + r.Get("/finalized/at/{height:\\d+}", wrapper(a.FinalizedHeightAt)) r.Route("/headers", func(r chi.Router) { r.Get("/last", wrapper(a.BlocksHeadersLast)) @@ -116,6 +120,11 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { }) }) + // Finalization generators. + r.Route("/generators", func(r chi.Router) { + r.Get("/at/{height:\\d+}", wrapper(a.GeneratorsAt)) + }) + r.Route("/assets", func(r chi.Router) { r.Get("/details/{id}", wrapper(a.AssetsDetailsByID)) r.Get("/details", wrapper(a.AssetsDetailsByIDsGet)) @@ -136,6 +145,7 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { r.Get("/unconfirmed/size", wrapper(a.unconfirmedSize)) r.Get("/info/{id}", wrapper(a.TransactionInfo)) r.Post("/broadcast", wrapper(a.TransactionsBroadcast)) + r.Post("/sign", wrapper(a.TransactionsSignCommit)) }) r.Route("/peers", func(r chi.Router) { diff --git a/pkg/mock/state.go b/pkg/mock/state.go index 92d52fe82e..069720275a 100644 --- a/pkg/mock/state.go +++ b/pkg/mock/state.go @@ -404,6 +404,21 @@ func (mr *MockStateInfoMockRecorder) EstimatorVersion() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimatorVersion", reflect.TypeOf((*MockStateInfo)(nil).EstimatorVersion)) } +// FinalizedHeightAt mocks base method. +func (m *MockStateInfo) FinalizedHeightAt(height proto.Height) (proto.Height, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FinalizedHeightAt", height) + ret0, _ := ret[0].(proto.Height) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FinalizedHeightAt indicates an expected call of FinalizedHeightAt. +func (mr *MockStateInfoMockRecorder) FinalizedHeightAt(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizedHeightAt", reflect.TypeOf((*MockStateInfo)(nil).FinalizedHeightAt), height) +} + // FindEndorserPKByIndex mocks base method. func (m *MockStateInfo) FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) { m.ctrl.T.Helper() @@ -674,6 +689,36 @@ func (mr *MockStateInfoMockRecorder) IsAssetExist(assetID interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAssetExist", reflect.TypeOf((*MockStateInfo)(nil).IsAssetExist), assetID) } +// LastFinalizedBlock mocks base method. +func (m *MockStateInfo) LastFinalizedBlock() (*proto.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LastFinalizedBlock") + ret0, _ := ret[0].(*proto.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LastFinalizedBlock indicates an expected call of LastFinalizedBlock. +func (mr *MockStateInfoMockRecorder) LastFinalizedBlock() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastFinalizedBlock", reflect.TypeOf((*MockStateInfo)(nil).LastFinalizedBlock)) +} + +// LastFinalizedHeight mocks base method. +func (m *MockStateInfo) LastFinalizedHeight() (proto.Height, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LastFinalizedHeight") + ret0, _ := ret[0].(proto.Height) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LastFinalizedHeight indicates an expected call of LastFinalizedHeight. +func (mr *MockStateInfoMockRecorder) LastFinalizedHeight() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastFinalizedHeight", reflect.TypeOf((*MockStateInfo)(nil).LastFinalizedHeight)) +} + // LegacyStateHashAtHeight mocks base method. func (m *MockStateInfo) LegacyStateHashAtHeight(height proto.Height) (*proto.StateHash, error) { m.ctrl.T.Helper() @@ -1912,6 +1957,21 @@ func (mr *MockStateMockRecorder) EstimatorVersion() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimatorVersion", reflect.TypeOf((*MockState)(nil).EstimatorVersion)) } +// FinalizedHeightAt mocks base method. +func (m *MockState) FinalizedHeightAt(height proto.Height) (proto.Height, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FinalizedHeightAt", height) + ret0, _ := ret[0].(proto.Height) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FinalizedHeightAt indicates an expected call of FinalizedHeightAt. +func (mr *MockStateMockRecorder) FinalizedHeightAt(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizedHeightAt", reflect.TypeOf((*MockState)(nil).FinalizedHeightAt), height) +} + // FindEndorserPKByIndex mocks base method. func (m *MockState) FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) { m.ctrl.T.Helper() @@ -2182,6 +2242,36 @@ func (mr *MockStateMockRecorder) IsAssetExist(assetID interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAssetExist", reflect.TypeOf((*MockState)(nil).IsAssetExist), assetID) } +// LastFinalizedBlock mocks base method. +func (m *MockState) LastFinalizedBlock() (*proto.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LastFinalizedBlock") + ret0, _ := ret[0].(*proto.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LastFinalizedBlock indicates an expected call of LastFinalizedBlock. +func (mr *MockStateMockRecorder) LastFinalizedBlock() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastFinalizedBlock", reflect.TypeOf((*MockState)(nil).LastFinalizedBlock)) +} + +// LastFinalizedHeight mocks base method. +func (m *MockState) LastFinalizedHeight() (proto.Height, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LastFinalizedHeight") + ret0, _ := ret[0].(proto.Height) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LastFinalizedHeight indicates an expected call of LastFinalizedHeight. +func (mr *MockStateMockRecorder) LastFinalizedHeight() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastFinalizedHeight", reflect.TypeOf((*MockState)(nil).LastFinalizedHeight)) +} + // LegacyStateHashAtHeight mocks base method. func (m *MockState) LegacyStateHashAtHeight(height proto.Height) (*proto.StateHash, error) { m.ctrl.T.Helper() diff --git a/pkg/state/api.go b/pkg/state/api.go index 1c1e42081b..34add4114d 100644 --- a/pkg/state/api.go +++ b/pkg/state/api.go @@ -164,6 +164,9 @@ type StateInfo interface { FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) FindGeneratorPKByEndorserPK(periodStart uint32, endorserPK bls.PublicKey) (crypto.PublicKey, error) CommittedGenerators(periodStart uint32) ([]proto.WavesAddress, error) + LastFinalizedHeight() (proto.Height, error) + LastFinalizedBlock() (*proto.Block, error) + FinalizedHeightAt(height proto.Height) (proto.Height, error) } // StateModifier contains all the methods needed to modify node's state. diff --git a/pkg/state/state.go b/pkg/state/state.go index ae7c1f1acb..e6f29c2eb1 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -3306,3 +3306,18 @@ func (s *stateManager) CommittedGenerators(periodStart uint32) ([]proto.WavesAdd } return addresses, nil } + +func (s *stateManager) LastFinalizedHeight() (proto.Height, error) { + // TODO implement this. + return 0, nil +} + +func (s *stateManager) LastFinalizedBlock() (*proto.Block, error) { + // TODO implement this. + return &proto.Block{}, nil +} + +func (s *stateManager) FinalizedHeightAt(_ proto.Height) (proto.Height, error) { + // TODO implement this. + return 0, nil +} diff --git a/pkg/state/threadsafe_wrapper.go b/pkg/state/threadsafe_wrapper.go index 860560fa26..998f07b88e 100644 --- a/pkg/state/threadsafe_wrapper.go +++ b/pkg/state/threadsafe_wrapper.go @@ -445,6 +445,24 @@ func (a *ThreadSafeReadWrapper) CommittedGenerators(periodStart uint32) ([]proto return a.s.CommittedGenerators(periodStart) } +func (a *ThreadSafeReadWrapper) LastFinalizedHeight() (proto.Height, error) { + a.mu.RLock() + defer a.mu.RUnlock() + return a.s.LastFinalizedHeight() +} + +func (a *ThreadSafeReadWrapper) LastFinalizedBlock() (*proto.Block, error) { + a.mu.RLock() + defer a.mu.RUnlock() + return a.s.LastFinalizedBlock() +} + +func (a *ThreadSafeReadWrapper) FinalizedHeightAt(height proto.Height) (proto.Height, error) { + a.mu.RLock() + defer a.mu.RUnlock() + return a.s.FinalizedHeightAt(height) +} + func NewThreadSafeReadWrapper(mu *sync.RWMutex, s StateInfo) StateInfo { return &ThreadSafeReadWrapper{ mu: mu, From 4be1b1ee6dac3b434c03e38a6aa074e5e077cce5 Mon Sep 17 00:00:00 2001 From: esuwu Date: Wed, 19 Nov 2025 17:44:28 +0100 Subject: [PATCH 02/17] Updated protobuf structures --- pkg/grpc/protobuf-schemas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/grpc/protobuf-schemas b/pkg/grpc/protobuf-schemas index 0a996a0762..f0e15d75b5 160000 --- a/pkg/grpc/protobuf-schemas +++ b/pkg/grpc/protobuf-schemas @@ -1 +1 @@ -Subproject commit 0a996a0762bbe28affb1c90fb8f2dcdb2e1cdad4 +Subproject commit f0e15d75b5ecd6a710c334abcb09450f370d034a From d033dc97ab039d79b1fd16c44504521bc800e63a Mon Sep 17 00:00:00 2001 From: esuwu Date: Mon, 24 Nov 2025 09:43:02 +0100 Subject: [PATCH 03/17] Added finalization storage --- pkg/api/node_api.go | 14 ---- pkg/api/routes.go | 1 - pkg/grpc/server/blockchain_api_test.go | 2 +- pkg/miner/endorsementpool/endorsement_pool.go | 3 +- pkg/node/blocks_applier/blocks_applier.go | 12 +-- pkg/node/fsm/ng_state.go | 5 +- pkg/state/address_transactions_test.go | 2 +- pkg/state/api.go | 3 +- pkg/state/finalization.go | 79 +++++++++++++++++++ pkg/state/history_storage.go | 1 + pkg/state/state.go | 35 +++++--- pkg/state/threadsafe_wrapper.go | 11 +-- pkg/types/types.go | 2 +- 13 files changed, 121 insertions(+), 49 deletions(-) create mode 100644 pkg/state/finalization.go diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index d0bd6a5329..9187d217f7 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -977,20 +977,6 @@ func (a *NodeApi) FinalizedHeader(w http.ResponseWriter, _ *http.Request) error return trySendJSON(w, blockHeader) } -func (a *NodeApi) FinalizedHeightAt(w http.ResponseWriter, r *http.Request) error { - heightStr := chi.URLParam(r, "height") - height, err := strconv.ParseUint(heightStr, 10, 64) - if err != nil { - return errors.Wrap(err, "invalid height") - } - - h, err := a.state.FinalizedHeightAt(height) - if err != nil { - return err - } - return trySendJSON(w, map[string]uint64{"height": h}) -} - func (a *NodeApi) TransactionsSignCommit(_ http.ResponseWriter, _ *http.Request) error { return nil } diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 176bcf0140..ef27c96788 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -110,7 +110,6 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { // Finalization. r.Get("/height/finalized", wrapper(a.FinalizedHeight)) r.Get("/headers/finalized", wrapper(a.FinalizedHeader)) - r.Get("/finalized/at/{height:\\d+}", wrapper(a.FinalizedHeightAt)) r.Route("/headers", func(r chi.Router) { r.Get("/last", wrapper(a.BlocksHeadersLast)) diff --git a/pkg/grpc/server/blockchain_api_test.go b/pkg/grpc/server/blockchain_api_test.go index 3d1a588d21..0cc9e3488a 100644 --- a/pkg/grpc/server/blockchain_api_test.go +++ b/pkg/grpc/server/blockchain_api_test.go @@ -35,7 +35,7 @@ func TestGetBaseTarget(t *testing.T) { newTarget := 171657201 blocks, err := state.ReadMainnetBlocksToHeight(proto.Height(3)) assert.NoError(t, err) - _, err = st.AddDeserializedBlocks(blocks) + _, err = st.AddDeserializedBlocks(blocks, false) assert.NoError(t, err) // Check new base target. res, err = cl.GetBaseTarget(ctx, &emptypb.Empty{}) diff --git a/pkg/miner/endorsementpool/endorsement_pool.go b/pkg/miner/endorsementpool/endorsement_pool.go index ac1a6524fc..eef73e03f2 100644 --- a/pkg/miner/endorsementpool/endorsement_pool.go +++ b/pkg/miner/endorsementpool/endorsement_pool.go @@ -133,7 +133,7 @@ func (p *EndorsementPool) GetAll() []proto.EndorseBlock { return out } -func (p *EndorsementPool) Finalize() (proto.FinalizationVoting, error) { +func (p *EndorsementPool) Finalize(finalizedBlockHeight proto.Height) (proto.FinalizationVoting, error) { p.mu.Lock() defer p.mu.Unlock() @@ -160,6 +160,7 @@ func (p *EndorsementPool) Finalize() (proto.FinalizationVoting, error) { return proto.FinalizationVoting{ AggregatedEndorsementSignature: aggregatedSignature, EndorserIndexes: endorsersIndexes, + FinalizedBlockHeight: finalizedBlockHeight, ConflictEndorsements: p.conflicts, }, nil } diff --git a/pkg/node/blocks_applier/blocks_applier.go b/pkg/node/blocks_applier/blocks_applier.go index 63a158fc53..6ba52f89a1 100644 --- a/pkg/node/blocks_applier/blocks_applier.go +++ b/pkg/node/blocks_applier/blocks_applier.go @@ -21,7 +21,7 @@ type innerState interface { Height() (proto.Height, error) ScoreAtHeight(height proto.Height) (*big.Int, error) BlockIDToHeight(blockID proto.BlockID) (proto.Height, error) - AddDeserializedBlocks(blocks []*proto.Block) (*proto.Block, error) + AddDeserializedBlocks(blocks []*proto.Block, isMicro bool) (*proto.Block, error) AddDeserializedBlocksWithSnapshots(blocks []*proto.Block, snapshots []*proto.BlockSnapshot) (*proto.Block, error) BlockByHeight(height proto.Height) (*proto.Block, error) RollbackToHeight(height proto.Height) error @@ -55,7 +55,7 @@ func (a *innerBlocksApplier) apply( // Do we need rollback? if parentHeight == currentHeight { // no, don't rollback, just add blocks - _, err = storage.AddDeserializedBlocks(blocks) + _, err = storage.AddDeserializedBlocks(blocks, false) if err != nil { return 0, err } @@ -79,10 +79,10 @@ func (a *innerBlocksApplier) apply( return 0, errors.Wrapf(err, "failed to rollback to height %d", parentHeight) } // applying new blocks - _, err = storage.AddDeserializedBlocks(blocks) + _, err = storage.AddDeserializedBlocks(blocks, false) if err != nil { // return back saved blocks - _, err2 := storage.AddDeserializedBlocks(rollbackBlocks) + _, err2 := storage.AddDeserializedBlocks(rollbackBlocks, false) if err2 != nil { return 0, errors.Wrap(err2, "failed rollback deserialized blocks") } @@ -269,10 +269,10 @@ func (a *innerBlocksApplier) applyMicro( } // applying new blocks - _, err = storage.AddDeserializedBlocks([]*proto.Block{block}) + _, err = storage.AddDeserializedBlocks([]*proto.Block{block}, true) if err != nil { // return back saved blocks - _, errAdd := storage.AddDeserializedBlocks([]*proto.Block{currentBlock}) + _, errAdd := storage.AddDeserializedBlocks([]*proto.Block{currentBlock}, true) if errAdd != nil { return 0, errors.Wrap(errAdd, "failed rollback block") } diff --git a/pkg/node/fsm/ng_state.go b/pkg/node/fsm/ng_state.go index 19e9d2c215..b3d9974f96 100644 --- a/pkg/node/fsm/ng_state.go +++ b/pkg/node/fsm/ng_state.go @@ -302,11 +302,12 @@ func (a *NGState) handleFinality(block *proto.Block, height proto.Height) error } if canFinalize { - finalization, finErr := a.baseInfo.endorsements.Finalize() + finalization, finErr := a.baseInfo.endorsements.Finalize(height) if finErr != nil { return finErr } block.FinalizationVoting = &finalization + block.FinalizationVoting.FinalizedBlockHeight = height } return nil } @@ -411,7 +412,7 @@ func (a *NGState) mineMicro( } var partialFinalization *proto.FinalizationVoting if finalityActivated { - fin, finErr := a.baseInfo.endorsements.Finalize() + fin, finErr := a.baseInfo.endorsements.Finalize(height) if finErr != nil { return a, nil, a.Errorf(errors.Wrap(finErr, "failed to finalize endorsements for microblock")) } diff --git a/pkg/state/address_transactions_test.go b/pkg/state/address_transactions_test.go index e24b6d5da5..97cf0fd3d9 100644 --- a/pkg/state/address_transactions_test.go +++ b/pkg/state/address_transactions_test.go @@ -25,7 +25,7 @@ func testIterImpl(t *testing.T, params StateParams) { blocks, err := ReadMainnetBlocksToHeight(blockHeight) require.NoError(t, err) // Add extra blocks and rollback to check that rollback scenario is handled correctly. - _, err = st.AddDeserializedBlocks(blocks) + _, err = st.AddDeserializedBlocks(blocks, false) require.NoError(t, err) err = st.RollbackToHeight(8000) require.NoError(t, err) diff --git a/pkg/state/api.go b/pkg/state/api.go index 34add4114d..fbf759c5fc 100644 --- a/pkg/state/api.go +++ b/pkg/state/api.go @@ -166,7 +166,6 @@ type StateInfo interface { CommittedGenerators(periodStart uint32) ([]proto.WavesAddress, error) LastFinalizedHeight() (proto.Height, error) LastFinalizedBlock() (*proto.Block, error) - FinalizedHeightAt(height proto.Height) (proto.Height, error) } // StateModifier contains all the methods needed to modify node's state. @@ -181,7 +180,7 @@ type StateModifier interface { AddBlocks(blocks [][]byte) error AddBlocksWithSnapshots(blocks [][]byte, snapshots []*proto.BlockSnapshot) error // AddDeserializedBlocks marshals blocks to binary and calls AddBlocks. - AddDeserializedBlocks(blocks []*proto.Block) (*proto.Block, error) + AddDeserializedBlocks(blocks []*proto.Block, isMicro bool) (*proto.Block, error) AddDeserializedBlocksWithSnapshots(blocks []*proto.Block, snapshots []*proto.BlockSnapshot) (*proto.Block, error) // Rollback functionality. RollbackToHeight(height proto.Height) error diff --git a/pkg/state/finalization.go b/pkg/state/finalization.go new file mode 100644 index 0000000000..ab0805c103 --- /dev/null +++ b/pkg/state/finalization.go @@ -0,0 +1,79 @@ +package state + +import ( + "fmt" + "github.com/fxamacker/cbor/v2" + "github.com/wavesplatform/gowaves/pkg/proto" +) + +const finalizationKey = "finalization" + +type finalizationItem struct { + Block proto.Block `cbor:"0,keyasint,omitempty"` + FinalizedBlockHeight proto.Height `cbor:"1,keyasint,omitempty"` +} + +type finalizationRecord struct { + Records []finalizationItem `cbor:"0,keyasint,omitempty"` +} + +func (fr *finalizationRecord) append(Block proto.Block, finalizedBlockHeight proto.Height) { + fr.Records = append(fr.Records, finalizationItem{ + Block: Block, + FinalizedBlockHeight: finalizedBlockHeight, + }) +} + +func (fr *finalizationRecord) marshalBinary() ([]byte, error) { return cbor.Marshal(fr) } +func (fr *finalizationRecord) unmarshalBinary(data []byte) error { return cbor.Unmarshal(data, fr) } + +type finalizations struct { + hs *historyStorage +} + +func newFinalizations(hs *historyStorage) *finalizations { + return &finalizations{hs: hs} +} + +func (f *finalizations) store(block proto.Block, finalizedBlockHeight proto.Height, currentBlockID proto.BlockID) error { + key := []byte(finalizationKey) + data, err := f.hs.newestTopEntryData(key) + if err != nil && !isNotFoundInHistoryOrDBErr(err) { + return fmt.Errorf("failed to retrieve finalization record: %w", err) + } + var rec finalizationRecord + if len(data) != 0 { + if umErr := rec.unmarshalBinary(data); umErr != nil { + return fmt.Errorf("failed to unmarshal finalization record: %w", umErr) + } + } + rec.append(block, finalizedBlockHeight) + newData, mErr := rec.marshalBinary() + if mErr != nil { + return fmt.Errorf("failed to marshal finalization record: %w", mErr) + } + if addErr := f.hs.addNewEntry(finalization, key, newData, currentBlockID); addErr != nil { + return fmt.Errorf("failed to add finalization record: %w", addErr) + } + return nil +} + +// newest returns the last finalized block (if exists). +func (f *finalizations) newest() (*finalizationItem, error) { + key := []byte(finalizationKey) + data, err := f.hs.newestTopEntryData(key) + if err != nil { + if isNotFoundInHistoryOrDBErr(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to retrieve finalization record: %w", err) + } + var rec finalizationRecord + if err := rec.unmarshalBinary(data); err != nil { + return nil, fmt.Errorf("failed to unmarshal finalization record: %w", err) + } + if len(rec.Records) == 0 { + return nil, nil + } + return &rec.Records[len(rec.Records)-1], nil +} diff --git a/pkg/state/history_storage.go b/pkg/state/history_storage.go index 8dffe12330..1609254ffb 100644 --- a/pkg/state/history_storage.go +++ b/pkg/state/history_storage.go @@ -46,6 +46,7 @@ const ( patches challengedAddress commitment + finalization ) type blockchainEntityProperties struct { diff --git a/pkg/state/state.go b/pkg/state/state.go index e6f29c2eb1..023615848b 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -71,6 +71,7 @@ type blockchainEntitiesStorage struct { snapshots *snapshotsAtHeight patches *patchesStorage commitments *commitments + finalizations *finalizations calculateHashes bool } @@ -106,6 +107,7 @@ func newBlockchainEntitiesStorage(hs *historyStorage, sets *settings.BlockchainS newSnapshotsAtHeight(hs, sets.AddressSchemeCharacter), newPatchesStorage(hs, sets.AddressSchemeCharacter), newCommitments(hs), + newFinalizations(hs), calcHashes, }, nil } @@ -1471,8 +1473,7 @@ func (s *stateManager) AddBlocksWithSnapshots(blockBytes [][]byte, snapshots []* } func (s *stateManager) AddDeserializedBlocks( - blocks []*proto.Block, -) (*proto.Block, error) { + blocks []*proto.Block, isMicro bool) (*proto.Block, error) { s.newBlocks.setNew(blocks) lastBlock, err := s.addBlocks() if err != nil { @@ -1484,6 +1485,17 @@ func (s *stateManager) AddDeserializedBlocks( } return nil, err } + + if !isMicro { + for _, block := range blocks { + if block != nil && block.FinalizationVoting != nil { + err := s.stor.finalizations.store(*block, block.FinalizationVoting.FinalizedBlockHeight, block.BlockID()) + if err != nil { + return nil, err + } + } + } + } return lastBlock, nil } @@ -3308,16 +3320,17 @@ func (s *stateManager) CommittedGenerators(periodStart uint32) ([]proto.WavesAdd } func (s *stateManager) LastFinalizedHeight() (proto.Height, error) { - // TODO implement this. - return 0, nil + item, err := s.stor.finalizations.newest() + if err != nil || item == nil { + return 0, err + } + return item.FinalizedBlockHeight, nil } func (s *stateManager) LastFinalizedBlock() (*proto.Block, error) { - // TODO implement this. - return &proto.Block{}, nil -} - -func (s *stateManager) FinalizedHeightAt(_ proto.Height) (proto.Height, error) { - // TODO implement this. - return 0, nil + item, err := s.stor.finalizations.newest() + if err != nil || item == nil { + return nil, err + } + return &item.Block, nil } diff --git a/pkg/state/threadsafe_wrapper.go b/pkg/state/threadsafe_wrapper.go index 998f07b88e..7ccadddd9b 100644 --- a/pkg/state/threadsafe_wrapper.go +++ b/pkg/state/threadsafe_wrapper.go @@ -457,12 +457,6 @@ func (a *ThreadSafeReadWrapper) LastFinalizedBlock() (*proto.Block, error) { return a.s.LastFinalizedBlock() } -func (a *ThreadSafeReadWrapper) FinalizedHeightAt(height proto.Height) (proto.Height, error) { - a.mu.RLock() - defer a.mu.RUnlock() - return a.s.FinalizedHeightAt(height) -} - func NewThreadSafeReadWrapper(mu *sync.RWMutex, s StateInfo) StateInfo { return &ThreadSafeReadWrapper{ mu: mu, @@ -528,11 +522,10 @@ func (a *ThreadSafeWriteWrapper) AddBlocksWithSnapshots(blocks [][]byte, snapsho } func (a *ThreadSafeWriteWrapper) AddDeserializedBlocks( - blocks []*proto.Block, -) (*proto.Block, error) { + blocks []*proto.Block, isMicro bool) (*proto.Block, error) { a.lock() defer a.unlock() - return a.s.AddDeserializedBlocks(blocks) + return a.s.AddDeserializedBlocks(blocks, isMicro) } func (a *ThreadSafeWriteWrapper) AddDeserializedBlocksWithSnapshots( diff --git a/pkg/types/types.go b/pkg/types/types.go index ab9620e0c2..5a50e079ff 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -231,7 +231,7 @@ type EndorsementPool interface { Add(e *proto.EndorseBlock, endorserPublicKey bls.PublicKey, balance uint64) error GetAll() []proto.EndorseBlock GetEndorsers() []bls.PublicKey - Finalize() (proto.FinalizationVoting, error) + Finalize(finalizedBlockHeight proto.Height) (proto.FinalizationVoting, error) Verify() (bool, error) Len() int CleanAll() From 402ef6feac830210f618277fa3cf9047bc833cd1 Mon Sep 17 00:00:00 2001 From: esuwu Date: Mon, 24 Nov 2025 10:07:21 +0100 Subject: [PATCH 04/17] Fixed errors --- pkg/grpc/server/blocks_api_test.go | 6 +-- pkg/mock/state.go | 46 ++++--------------- .../blocks_applier/blocks_applier_test.go | 4 +- pkg/node/blocks_applier/node_mocks.go | 2 +- pkg/state/finalization.go | 20 +++++--- pkg/state/state.go | 6 +-- 6 files changed, 30 insertions(+), 54 deletions(-) diff --git a/pkg/grpc/server/blocks_api_test.go b/pkg/grpc/server/blocks_api_test.go index 6249274e99..d07a8e3033 100644 --- a/pkg/grpc/server/blocks_api_test.go +++ b/pkg/grpc/server/blocks_api_test.go @@ -53,7 +53,7 @@ func TestGetBlock(t *testing.T) { blockHeight := proto.Height(99) blocks, err := state.ReadMainnetBlocksToHeight(blockHeight) assert.NoError(t, err) - _, err = st.AddDeserializedBlocks(blocks) + _, err = st.AddDeserializedBlocks(blocks, false) assert.NoError(t, err) // Retrieve expected block. correctBlockProto := blockFromState(t, blockHeight, st) @@ -100,7 +100,7 @@ func TestGetBlockRange(t *testing.T) { blockHeight := proto.Height(99) blocks, err := state.ReadMainnetBlocksToHeight(blockHeight) assert.NoError(t, err) - _, err = st.AddDeserializedBlocks(blocks) + _, err = st.AddDeserializedBlocks(blocks, false) assert.NoError(t, err) // With transactions. @@ -174,7 +174,7 @@ func TestGetCurrentHeight(t *testing.T) { blockHeight := proto.Height(99) blocks, err := state.ReadMainnetBlocksToHeight(blockHeight) assert.NoError(t, err) - _, err = st.AddDeserializedBlocks(blocks) + _, err = st.AddDeserializedBlocks(blocks, false) assert.NoError(t, err) res, err = cl.GetCurrentHeight(ctx, &emptypb.Empty{}) diff --git a/pkg/mock/state.go b/pkg/mock/state.go index 069720275a..de4c875fbd 100644 --- a/pkg/mock/state.go +++ b/pkg/mock/state.go @@ -404,21 +404,6 @@ func (mr *MockStateInfoMockRecorder) EstimatorVersion() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimatorVersion", reflect.TypeOf((*MockStateInfo)(nil).EstimatorVersion)) } -// FinalizedHeightAt mocks base method. -func (m *MockStateInfo) FinalizedHeightAt(height proto.Height) (proto.Height, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FinalizedHeightAt", height) - ret0, _ := ret[0].(proto.Height) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FinalizedHeightAt indicates an expected call of FinalizedHeightAt. -func (mr *MockStateInfoMockRecorder) FinalizedHeightAt(height interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizedHeightAt", reflect.TypeOf((*MockStateInfo)(nil).FinalizedHeightAt), height) -} - // FindEndorserPKByIndex mocks base method. func (m *MockStateInfo) FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) { m.ctrl.T.Helper() @@ -1311,18 +1296,18 @@ func (mr *MockStateModifierMockRecorder) AddDeserializedBlock(block interface{}) } // AddDeserializedBlocks mocks base method. -func (m *MockStateModifier) AddDeserializedBlocks(blocks []*proto.Block) (*proto.Block, error) { +func (m *MockStateModifier) AddDeserializedBlocks(blocks []*proto.Block, isMicro bool) (*proto.Block, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddDeserializedBlocks", blocks) + ret := m.ctrl.Call(m, "AddDeserializedBlocks", blocks, isMicro) ret0, _ := ret[0].(*proto.Block) ret1, _ := ret[1].(error) return ret0, ret1 } // AddDeserializedBlocks indicates an expected call of AddDeserializedBlocks. -func (mr *MockStateModifierMockRecorder) AddDeserializedBlocks(blocks interface{}) *gomock.Call { +func (mr *MockStateModifierMockRecorder) AddDeserializedBlocks(blocks, isMicro interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDeserializedBlocks", reflect.TypeOf((*MockStateModifier)(nil).AddDeserializedBlocks), blocks) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDeserializedBlocks", reflect.TypeOf((*MockStateModifier)(nil).AddDeserializedBlocks), blocks, isMicro) } // AddDeserializedBlocksWithSnapshots mocks base method. @@ -1629,18 +1614,18 @@ func (mr *MockStateMockRecorder) AddDeserializedBlock(block interface{}) *gomock } // AddDeserializedBlocks mocks base method. -func (m *MockState) AddDeserializedBlocks(blocks []*proto.Block) (*proto.Block, error) { +func (m *MockState) AddDeserializedBlocks(blocks []*proto.Block, isMicro bool) (*proto.Block, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddDeserializedBlocks", blocks) + ret := m.ctrl.Call(m, "AddDeserializedBlocks", blocks, isMicro) ret0, _ := ret[0].(*proto.Block) ret1, _ := ret[1].(error) return ret0, ret1 } // AddDeserializedBlocks indicates an expected call of AddDeserializedBlocks. -func (mr *MockStateMockRecorder) AddDeserializedBlocks(blocks interface{}) *gomock.Call { +func (mr *MockStateMockRecorder) AddDeserializedBlocks(blocks, isMicro interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDeserializedBlocks", reflect.TypeOf((*MockState)(nil).AddDeserializedBlocks), blocks) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDeserializedBlocks", reflect.TypeOf((*MockState)(nil).AddDeserializedBlocks), blocks, isMicro) } // AddDeserializedBlocksWithSnapshots mocks base method. @@ -1957,21 +1942,6 @@ func (mr *MockStateMockRecorder) EstimatorVersion() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimatorVersion", reflect.TypeOf((*MockState)(nil).EstimatorVersion)) } -// FinalizedHeightAt mocks base method. -func (m *MockState) FinalizedHeightAt(height proto.Height) (proto.Height, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FinalizedHeightAt", height) - ret0, _ := ret[0].(proto.Height) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FinalizedHeightAt indicates an expected call of FinalizedHeightAt. -func (mr *MockStateMockRecorder) FinalizedHeightAt(height interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizedHeightAt", reflect.TypeOf((*MockState)(nil).FinalizedHeightAt), height) -} - // FindEndorserPKByIndex mocks base method. func (m *MockState) FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) { m.ctrl.T.Helper() diff --git a/pkg/node/blocks_applier/blocks_applier_test.go b/pkg/node/blocks_applier/blocks_applier_test.go index 225bc4eadf..9e61922b4c 100644 --- a/pkg/node/blocks_applier/blocks_applier_test.go +++ b/pkg/node/blocks_applier/blocks_applier_test.go @@ -98,9 +98,9 @@ func TestApply_InvalidBlockWithRollback(t *testing.T) { // rollback to first(genesis) block stateMock.EXPECT().RollbackToHeight(proto.Height(1)).Return(nil) // adding new blocks, and have error on applying - stateMock.EXPECT().AddDeserializedBlocks([]*proto.Block{block2}).Return(nil, errors.New("error message")) + stateMock.EXPECT().AddDeserializedBlocks([]*proto.Block{block2}, false).Return(nil, errors.New("error message")) // return blocks - stateMock.EXPECT().AddDeserializedBlocks([]*proto.Block{block1}).Return(nil, nil) + stateMock.EXPECT().AddDeserializedBlocks([]*proto.Block{block1}, false).Return(nil, nil) ba := innerBlocksApplier{} _, err := ba.apply(stateMock, []*proto.Block{block2}) diff --git a/pkg/node/blocks_applier/node_mocks.go b/pkg/node/blocks_applier/node_mocks.go index 2bcd46f182..a13e47edb4 100644 --- a/pkg/node/blocks_applier/node_mocks.go +++ b/pkg/node/blocks_applier/node_mocks.go @@ -278,7 +278,7 @@ func (a *MockStateManager) AddDeserializedBlock(block *proto.Block) (*proto.Bloc } func (a *MockStateManager) AddDeserializedBlocks( - blocks []*proto.Block, + blocks []*proto.Block, _ bool, ) (*proto.Block, error) { var out *proto.Block var err error diff --git a/pkg/state/finalization.go b/pkg/state/finalization.go index ab0805c103..e02f142d0f 100644 --- a/pkg/state/finalization.go +++ b/pkg/state/finalization.go @@ -2,12 +2,17 @@ package state import ( "fmt" + "github.com/fxamacker/cbor/v2" + "github.com/pkg/errors" "github.com/wavesplatform/gowaves/pkg/proto" ) const finalizationKey = "finalization" +var ErrNoFinalization = errors.New("no finalized blocks recorded") +var ErrNoFinalizationHistory = errors.New("no finalization in history") + type finalizationItem struct { Block proto.Block `cbor:"0,keyasint,omitempty"` FinalizedBlockHeight proto.Height `cbor:"1,keyasint,omitempty"` @@ -17,9 +22,9 @@ type finalizationRecord struct { Records []finalizationItem `cbor:"0,keyasint,omitempty"` } -func (fr *finalizationRecord) append(Block proto.Block, finalizedBlockHeight proto.Height) { +func (fr *finalizationRecord) append(block proto.Block, finalizedBlockHeight proto.Height) { fr.Records = append(fr.Records, finalizationItem{ - Block: Block, + Block: block, FinalizedBlockHeight: finalizedBlockHeight, }) } @@ -35,7 +40,8 @@ func newFinalizations(hs *historyStorage) *finalizations { return &finalizations{hs: hs} } -func (f *finalizations) store(block proto.Block, finalizedBlockHeight proto.Height, currentBlockID proto.BlockID) error { +func (f *finalizations) store(block proto.Block, finalizedBlockHeight proto.Height, + currentBlockID proto.BlockID) error { key := []byte(finalizationKey) data, err := f.hs.newestTopEntryData(key) if err != nil && !isNotFoundInHistoryOrDBErr(err) { @@ -64,16 +70,16 @@ func (f *finalizations) newest() (*finalizationItem, error) { data, err := f.hs.newestTopEntryData(key) if err != nil { if isNotFoundInHistoryOrDBErr(err) { - return nil, nil + return nil, ErrNoFinalizationHistory } return nil, fmt.Errorf("failed to retrieve finalization record: %w", err) } var rec finalizationRecord - if err := rec.unmarshalBinary(data); err != nil { - return nil, fmt.Errorf("failed to unmarshal finalization record: %w", err) + if unmrshhlErr := rec.unmarshalBinary(data); unmrshhlErr != nil { + return nil, fmt.Errorf("failed to unmarshal finalization record: %w", unmrshhlErr) } if len(rec.Records) == 0 { - return nil, nil + return nil, ErrNoFinalization } return &rec.Records[len(rec.Records)-1], nil } diff --git a/pkg/state/state.go b/pkg/state/state.go index 023615848b..49429e2656 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -1489,9 +1489,9 @@ func (s *stateManager) AddDeserializedBlocks( if !isMicro { for _, block := range blocks { if block != nil && block.FinalizationVoting != nil { - err := s.stor.finalizations.store(*block, block.FinalizationVoting.FinalizedBlockHeight, block.BlockID()) - if err != nil { - return nil, err + storeErr := s.stor.finalizations.store(*block, block.FinalizationVoting.FinalizedBlockHeight, block.BlockID()) + if storeErr != nil { + return nil, storeErr } } } From 1caf2a1cedf8ec24cd632dc251ddbec86954749d Mon Sep 17 00:00:00 2001 From: esuwu Date: Mon, 24 Nov 2025 10:12:32 +0100 Subject: [PATCH 05/17] Fixed old code --- cmd/wallet/main.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/wallet/main.go b/cmd/wallet/main.go index ec707c7658..3565777c90 100644 --- a/cmd/wallet/main.go +++ b/cmd/wallet/main.go @@ -449,11 +449,11 @@ func createWallet( return errors.Wrap(err, "failed to write the wallet's data to the wallet") } - fmt.Printf("New account has been to wallet successfully %s\n", walletPath) - fmt.Printf("Account Seed: %s\n", walletCredentials.accountSeed.String()) - fmt.Printf("Public Key: %s\n", walletCredentials.pk.String()) - fmt.Printf("Secret Key: %s\n", walletCredentials.sk.String()) - fmt.Printf("Address: %s\n", walletCredentials.address.String()) + fmt.Fprintf(os.Stdout, "New account has been added to wallet successfully %s\n", walletPath) + fmt.Fprintf(os.Stdout, "Account Seed: %s\n", walletCredentials.accountSeed.String()) + fmt.Fprintf(os.Stdout, "Public Key: %s\n", walletCredentials.pk.String()) + fmt.Fprintf(os.Stdout, "Secret Key: %s\n", walletCredentials.sk.String()) + fmt.Fprintf(os.Stdout, "Address: %s\n", walletCredentials.address.String()) return nil } From 82a7a2ef20ef909449da7704a54b743e72e7f1d6 Mon Sep 17 00:00:00 2001 From: esuwu Date: Mon, 24 Nov 2025 11:27:32 +0100 Subject: [PATCH 06/17] SignCommitGeneration draft --- pkg/api/node_api.go | 77 ++++++++++++++++++++++++++++++++- pkg/mock/state.go | 30 +++++++++++++ pkg/state/api.go | 1 + pkg/state/state.go | 32 ++++++++++++++ pkg/state/threadsafe_wrapper.go | 7 +++ pkg/types/types.go | 1 + pkg/wallet/embedded_wallet.go | 19 ++++++++ pkg/wallet/stub.go | 8 +++- 8 files changed, 171 insertions(+), 4 deletions(-) diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index 9187d217f7..f144f81e84 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -16,8 +16,10 @@ import ( "github.com/go-chi/chi/v5" "github.com/pkg/errors" + "github.com/ccoveille/go-safecast/v2" apiErrs "github.com/wavesplatform/gowaves/pkg/api/errors" "github.com/wavesplatform/gowaves/pkg/crypto" + "github.com/wavesplatform/gowaves/pkg/crypto/bls" "github.com/wavesplatform/gowaves/pkg/errs" "github.com/wavesplatform/gowaves/pkg/logging" "github.com/wavesplatform/gowaves/pkg/proto" @@ -977,8 +979,79 @@ func (a *NodeApi) FinalizedHeader(w http.ResponseWriter, _ *http.Request) error return trySendJSON(w, blockHeader) } -func (a *NodeApi) TransactionsSignCommit(_ http.ResponseWriter, _ *http.Request) error { - return nil +type SignCommitRequest struct { + Sender string `json:"sender"` + GenerationPeriodStart *uint32 `json:"generationPeriodStart,omitempty"` + Timestamp *int64 `json:"timestamp,omitempty"` + ChainID *byte `json:"chainId,omitempty"` +} + +func (a *NodeApi) TransactionsSignCommit(w http.ResponseWriter, r *http.Request) error { + var req SignCommitRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return errors.Wrap(err, "failed to decode JSON") + } + + addr, err := proto.NewAddressFromString(req.Sender) + if err != nil { + return errors.Wrap(err, "invalid sender address") + } + + now := time.Now().UnixMilli() + + timestamp := now + if req.Timestamp != nil { + timestamp = *req.Timestamp + } + timestampUint, err := safecast.Convert[uint64](timestamp) + if err != nil { + return errors.Wrap(err, "invalid timestamp") + } + + var periodStart uint32 + if req.GenerationPeriodStart != nil { + periodStart = *req.GenerationPeriodStart + } else { + height, retrieveErr := a.state.Height() + if retrieveErr != nil { + return errors.Wrap(retrieveErr, "failed to get height") + } + activationH, actErr := a.state.ActivationHeight(int16(settings.DeterministicFinality)) + if actErr != nil { + return errors.Wrap(actErr, "failed to get DF activation height") + } + + periodStart, err = state.CurrentGenerationPeriodStart(activationH, height, a.app.settings.GenerationPeriod) + if err != nil { + return errors.Wrap(err, "failed to calculate generationPeriodStart") + } + } + senderPK, err := a.app.services.Wallet.FindPublicKeyByAddress(addr, a.app.services.Scheme) + if err != nil { + return errors.Wrap(err, "failed to find key pair by address") + } + + endorserPK, err := a.state.FindEndorserPKByGeneratorPK(periodStart, senderPK) + if err != nil { + return errors.Wrap(err, "failed to find endorser public key by generator public key") + } + + var commitmentSignature bls.Signature // TODO fill it out. + + tx := proto.NewUnsignedCommitToGenerationWithProofs(1, + senderPK, + periodStart, + endorserPK, + commitmentSignature, + state.FeeUnit, + timestampUint) + + // TODO must be signed with the BLS key instead. + err = a.app.services.Wallet.SignTransactionWith(senderPK, tx) + if err != nil { + return err + } + return trySendJSON(w, tx) } func wavesAddressInvalidCharErr(invalidChar rune, id string) *apiErrs.CustomValidationError { diff --git a/pkg/mock/state.go b/pkg/mock/state.go index de4c875fbd..616d942ef2 100644 --- a/pkg/mock/state.go +++ b/pkg/mock/state.go @@ -404,6 +404,21 @@ func (mr *MockStateInfoMockRecorder) EstimatorVersion() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimatorVersion", reflect.TypeOf((*MockStateInfo)(nil).EstimatorVersion)) } +// FindEndorserPKByGeneratorPK mocks base method. +func (m *MockStateInfo) FindEndorserPKByGeneratorPK(periodStart uint32, generatorPK crypto.PublicKey) (bls.PublicKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindEndorserPKByGeneratorPK", periodStart, generatorPK) + ret0, _ := ret[0].(bls.PublicKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindEndorserPKByGeneratorPK indicates an expected call of FindEndorserPKByGeneratorPK. +func (mr *MockStateInfoMockRecorder) FindEndorserPKByGeneratorPK(periodStart, generatorPK interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindEndorserPKByGeneratorPK", reflect.TypeOf((*MockStateInfo)(nil).FindEndorserPKByGeneratorPK), periodStart, generatorPK) +} + // FindEndorserPKByIndex mocks base method. func (m *MockStateInfo) FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) { m.ctrl.T.Helper() @@ -1942,6 +1957,21 @@ func (mr *MockStateMockRecorder) EstimatorVersion() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimatorVersion", reflect.TypeOf((*MockState)(nil).EstimatorVersion)) } +// FindEndorserPKByGeneratorPK mocks base method. +func (m *MockState) FindEndorserPKByGeneratorPK(periodStart uint32, generatorPK crypto.PublicKey) (bls.PublicKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindEndorserPKByGeneratorPK", periodStart, generatorPK) + ret0, _ := ret[0].(bls.PublicKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindEndorserPKByGeneratorPK indicates an expected call of FindEndorserPKByGeneratorPK. +func (mr *MockStateMockRecorder) FindEndorserPKByGeneratorPK(periodStart, generatorPK interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindEndorserPKByGeneratorPK", reflect.TypeOf((*MockState)(nil).FindEndorserPKByGeneratorPK), periodStart, generatorPK) +} + // FindEndorserPKByIndex mocks base method. func (m *MockState) FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) { m.ctrl.T.Helper() diff --git a/pkg/state/api.go b/pkg/state/api.go index fbf759c5fc..9cf16b2014 100644 --- a/pkg/state/api.go +++ b/pkg/state/api.go @@ -163,6 +163,7 @@ type StateInfo interface { FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) FindGeneratorPKByEndorserPK(periodStart uint32, endorserPK bls.PublicKey) (crypto.PublicKey, error) + FindEndorserPKByGeneratorPK(periodStart uint32, generatorPK crypto.PublicKey) (bls.PublicKey, error) CommittedGenerators(periodStart uint32) ([]proto.WavesAddress, error) LastFinalizedHeight() (proto.Height, error) LastFinalizedBlock() (*proto.Block, error) diff --git a/pkg/state/state.go b/pkg/state/state.go index 49429e2656..9e20aaaa03 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -3334,3 +3334,35 @@ func (s *stateManager) LastFinalizedBlock() (*proto.Block, error) { } return &item.Block, nil } + +// FindEndorserPKByGeneratorPK finds the BLS endorser public key corresponding +// to the given Waves generator public key in the commitments record for the given period. +func (s *stateManager) FindEndorserPKByGeneratorPK(periodStart uint32, + generatorPK crypto.PublicKey) (bls.PublicKey, error) { + key := commitmentKey{periodStart: periodStart} + data, err := s.stor.commitments.hs.newestTopEntryData(key.bytes()) + if err != nil { + if errors.Is(err, keyvalue.ErrNotFound) { + return bls.PublicKey{}, errors.Errorf( + "no commitments found for period %d, %v", periodStart, err) + } + return bls.PublicKey{}, errors.Errorf( + "failed to retrieve commitments record: %v", err) + } + + var rec commitmentsRecord + if umErr := rec.unmarshalBinary(data); umErr != nil { + return bls.PublicKey{}, fmt.Errorf( + "failed to unmarshal commitments record: %w", umErr) + } + + genPKb := generatorPK.Bytes() + for _, cm := range rec.Commitments { + if bytes.Equal(genPKb, cm.GeneratorPK.Bytes()) { + return cm.EndorserPK, nil + } + } + + return bls.PublicKey{}, fmt.Errorf( + "generator public key not found in commitments for period %d", periodStart) +} diff --git a/pkg/state/threadsafe_wrapper.go b/pkg/state/threadsafe_wrapper.go index 7ccadddd9b..36132012f1 100644 --- a/pkg/state/threadsafe_wrapper.go +++ b/pkg/state/threadsafe_wrapper.go @@ -439,6 +439,13 @@ func (a *ThreadSafeReadWrapper) FindGeneratorPKByEndorserPK(periodStart uint32, return a.s.FindGeneratorPKByEndorserPK(periodStart, endorserPK) } +func (a *ThreadSafeReadWrapper) FindEndorserPKByGeneratorPK(periodStart uint32, + generatorPK crypto.PublicKey) (bls.PublicKey, error) { + a.mu.RLock() + defer a.mu.RUnlock() + return a.s.FindEndorserPKByGeneratorPK(periodStart, generatorPK) +} + func (a *ThreadSafeReadWrapper) CommittedGenerators(periodStart uint32) ([]proto.WavesAddress, error) { a.mu.RLock() defer a.mu.RUnlock() diff --git a/pkg/types/types.go b/pkg/types/types.go index 5a50e079ff..381187d11d 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -222,6 +222,7 @@ type MinerConsensus interface { type EmbeddedWallet interface { SignTransactionWith(pk crypto.PublicKey, tx proto.Transaction) error + FindPublicKeyByAddress(address proto.WavesAddress, scheme proto.Scheme) (crypto.PublicKey, error) Load(password []byte) error AccountSeeds() [][]byte } diff --git a/pkg/wallet/embedded_wallet.go b/pkg/wallet/embedded_wallet.go index b388608253..bcc5948f2a 100644 --- a/pkg/wallet/embedded_wallet.go +++ b/pkg/wallet/embedded_wallet.go @@ -32,6 +32,25 @@ func (a *EmbeddedWalletImpl) SignTransactionWith(pk crypto.PublicKey, tx proto.T return ErrPublicKeyNotFound } +func (a *EmbeddedWalletImpl) FindPublicKeyByAddress(address proto.WavesAddress, + scheme proto.Scheme) (crypto.PublicKey, error) { + seeds := a.seeder.AccountSeeds() + for _, s := range seeds { + _, public, err := crypto.GenerateKeyPair(s) + if err != nil { + return crypto.PublicKey{}, err + } + retrievedAddress, err := proto.NewAddressFromPublicKey(scheme, public) + if err != nil { + return crypto.PublicKey{}, err + } + if retrievedAddress == address { + return public, nil + } + } + return crypto.PublicKey{}, ErrPublicKeyNotFound +} + func (a *EmbeddedWalletImpl) Load(password []byte) error { bts, err := a.loader.Load() if err != nil { diff --git a/pkg/wallet/stub.go b/pkg/wallet/stub.go index ed617b46d3..1fc6889d74 100644 --- a/pkg/wallet/stub.go +++ b/pkg/wallet/stub.go @@ -9,11 +9,15 @@ type Stub struct { S [][]byte } -func (s Stub) SignTransactionWith(pk crypto.PublicKey, tx proto.Transaction) error { +func (s Stub) SignTransactionWith(_ crypto.PublicKey, _ proto.Transaction) error { panic("Stub.SignTransactionWith: Unsopported operation") } -func (s Stub) Load(password []byte) error { +func (s Stub) FindPublicKeyByAddress(_ proto.WavesAddress, _ proto.Scheme) (crypto.PublicKey, error) { + panic("Stub.FindPublicKeyByAddress: Unsupported operation") +} + +func (s Stub) Load(_ []byte) error { panic("Stub.Load: Unsupported operation") } From 350e93deb65057a824dd5855b4c5c49a1ef35982 Mon Sep 17 00:00:00 2001 From: esuwu Date: Mon, 24 Nov 2025 18:04:44 +0100 Subject: [PATCH 07/17] Finished signCommitToGeneration --- pkg/api/node_api.go | 14 ++++++-------- pkg/api/routes.go | 4 +++- pkg/mock/state.go | 30 ------------------------------ pkg/state/api.go | 1 - pkg/state/state.go | 32 -------------------------------- pkg/state/threadsafe_wrapper.go | 7 ------- pkg/types/types.go | 1 + pkg/wallet/embedded_wallet.go | 23 +++++++++++++++++++++++ pkg/wallet/stub.go | 5 +++++ 9 files changed, 38 insertions(+), 79 deletions(-) diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index 557b2d606e..824ad068c3 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -940,7 +940,6 @@ func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { TransactionID: "", // TODO should be somehow found. }) } - return trySendJSON(w, generatorsInfo) } @@ -1031,22 +1030,21 @@ func (a *NodeApi) TransactionsSignCommit(w http.ResponseWriter, r *http.Request) return errors.Wrap(err, "failed to find key pair by address") } - endorserPK, err := a.state.FindEndorserPKByGeneratorPK(periodStart, senderPK) + blsSecretKey, blsPublicKey, err := a.app.services.Wallet.BlsPairByWavesPK(senderPK) if err != nil { return errors.Wrap(err, "failed to find endorser public key by generator public key") } - - var commitmentSignature bls.Signature // TODO fill it out. - + _, commitmentSignature, popErr := bls.ProvePoP(blsSecretKey, blsPublicKey, periodStart) + if popErr != nil { + return errors.Wrap(popErr, "failed to create proof of possession for commitment") + } tx := proto.NewUnsignedCommitToGenerationWithProofs(1, senderPK, periodStart, - endorserPK, + blsPublicKey, commitmentSignature, state.FeeUnit, timestampUint) - - // TODO must be signed with the BLS key instead. err = a.app.services.Wallet.SignTransactionWith(senderPK, tx) if err != nil { return err diff --git a/pkg/api/routes.go b/pkg/api/routes.go index ef27c96788..72334e7f2e 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -144,7 +144,9 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { r.Get("/unconfirmed/size", wrapper(a.unconfirmedSize)) r.Get("/info/{id}", wrapper(a.TransactionInfo)) r.Post("/broadcast", wrapper(a.TransactionsBroadcast)) - r.Post("/sign", wrapper(a.TransactionsSignCommit)) + + rAuth := r.With(checkAuthMiddleware) + rAuth.Post("/sign", wrapper(a.TransactionsSignCommit)) }) r.Route("/peers", func(r chi.Router) { diff --git a/pkg/mock/state.go b/pkg/mock/state.go index 616d942ef2..de4c875fbd 100644 --- a/pkg/mock/state.go +++ b/pkg/mock/state.go @@ -404,21 +404,6 @@ func (mr *MockStateInfoMockRecorder) EstimatorVersion() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimatorVersion", reflect.TypeOf((*MockStateInfo)(nil).EstimatorVersion)) } -// FindEndorserPKByGeneratorPK mocks base method. -func (m *MockStateInfo) FindEndorserPKByGeneratorPK(periodStart uint32, generatorPK crypto.PublicKey) (bls.PublicKey, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindEndorserPKByGeneratorPK", periodStart, generatorPK) - ret0, _ := ret[0].(bls.PublicKey) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindEndorserPKByGeneratorPK indicates an expected call of FindEndorserPKByGeneratorPK. -func (mr *MockStateInfoMockRecorder) FindEndorserPKByGeneratorPK(periodStart, generatorPK interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindEndorserPKByGeneratorPK", reflect.TypeOf((*MockStateInfo)(nil).FindEndorserPKByGeneratorPK), periodStart, generatorPK) -} - // FindEndorserPKByIndex mocks base method. func (m *MockStateInfo) FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) { m.ctrl.T.Helper() @@ -1957,21 +1942,6 @@ func (mr *MockStateMockRecorder) EstimatorVersion() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EstimatorVersion", reflect.TypeOf((*MockState)(nil).EstimatorVersion)) } -// FindEndorserPKByGeneratorPK mocks base method. -func (m *MockState) FindEndorserPKByGeneratorPK(periodStart uint32, generatorPK crypto.PublicKey) (bls.PublicKey, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FindEndorserPKByGeneratorPK", periodStart, generatorPK) - ret0, _ := ret[0].(bls.PublicKey) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindEndorserPKByGeneratorPK indicates an expected call of FindEndorserPKByGeneratorPK. -func (mr *MockStateMockRecorder) FindEndorserPKByGeneratorPK(periodStart, generatorPK interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindEndorserPKByGeneratorPK", reflect.TypeOf((*MockState)(nil).FindEndorserPKByGeneratorPK), periodStart, generatorPK) -} - // FindEndorserPKByIndex mocks base method. func (m *MockState) FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) { m.ctrl.T.Helper() diff --git a/pkg/state/api.go b/pkg/state/api.go index 9cf16b2014..fbf759c5fc 100644 --- a/pkg/state/api.go +++ b/pkg/state/api.go @@ -163,7 +163,6 @@ type StateInfo interface { FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) FindGeneratorPKByEndorserPK(periodStart uint32, endorserPK bls.PublicKey) (crypto.PublicKey, error) - FindEndorserPKByGeneratorPK(periodStart uint32, generatorPK crypto.PublicKey) (bls.PublicKey, error) CommittedGenerators(periodStart uint32) ([]proto.WavesAddress, error) LastFinalizedHeight() (proto.Height, error) LastFinalizedBlock() (*proto.Block, error) diff --git a/pkg/state/state.go b/pkg/state/state.go index 4fc5ff3be4..94cfdc9585 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -3409,35 +3409,3 @@ func (s *stateManager) LastFinalizedBlock() (*proto.Block, error) { } return &item.Block, nil } - -// FindEndorserPKByGeneratorPK finds the BLS endorser public key corresponding -// to the given Waves generator public key in the commitments record for the given period. -func (s *stateManager) FindEndorserPKByGeneratorPK(periodStart uint32, - generatorPK crypto.PublicKey) (bls.PublicKey, error) { - key := commitmentKey{periodStart: periodStart} - data, err := s.stor.commitments.hs.newestTopEntryData(key.bytes()) - if err != nil { - if errors.Is(err, keyvalue.ErrNotFound) { - return bls.PublicKey{}, errors.Errorf( - "no commitments found for period %d, %v", periodStart, err) - } - return bls.PublicKey{}, errors.Errorf( - "failed to retrieve commitments record: %v", err) - } - - var rec commitmentsRecord - if umErr := rec.unmarshalBinary(data); umErr != nil { - return bls.PublicKey{}, fmt.Errorf( - "failed to unmarshal commitments record: %w", umErr) - } - - genPKb := generatorPK.Bytes() - for _, cm := range rec.Commitments { - if bytes.Equal(genPKb, cm.GeneratorPK.Bytes()) { - return cm.EndorserPK, nil - } - } - - return bls.PublicKey{}, fmt.Errorf( - "generator public key not found in commitments for period %d", periodStart) -} diff --git a/pkg/state/threadsafe_wrapper.go b/pkg/state/threadsafe_wrapper.go index 36132012f1..7ccadddd9b 100644 --- a/pkg/state/threadsafe_wrapper.go +++ b/pkg/state/threadsafe_wrapper.go @@ -439,13 +439,6 @@ func (a *ThreadSafeReadWrapper) FindGeneratorPKByEndorserPK(periodStart uint32, return a.s.FindGeneratorPKByEndorserPK(periodStart, endorserPK) } -func (a *ThreadSafeReadWrapper) FindEndorserPKByGeneratorPK(periodStart uint32, - generatorPK crypto.PublicKey) (bls.PublicKey, error) { - a.mu.RLock() - defer a.mu.RUnlock() - return a.s.FindEndorserPKByGeneratorPK(periodStart, generatorPK) -} - func (a *ThreadSafeReadWrapper) CommittedGenerators(periodStart uint32) ([]proto.WavesAddress, error) { a.mu.RLock() defer a.mu.RUnlock() diff --git a/pkg/types/types.go b/pkg/types/types.go index 381187d11d..dbe99ac92b 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -223,6 +223,7 @@ type MinerConsensus interface { type EmbeddedWallet interface { SignTransactionWith(pk crypto.PublicKey, tx proto.Transaction) error FindPublicKeyByAddress(address proto.WavesAddress, scheme proto.Scheme) (crypto.PublicKey, error) + BlsPairByWavesPK(publicKey crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) Load(password []byte) error AccountSeeds() [][]byte } diff --git a/pkg/wallet/embedded_wallet.go b/pkg/wallet/embedded_wallet.go index bcc5948f2a..fdd529aeb8 100644 --- a/pkg/wallet/embedded_wallet.go +++ b/pkg/wallet/embedded_wallet.go @@ -4,6 +4,7 @@ import ( "sync" "github.com/wavesplatform/gowaves/pkg/crypto" + "github.com/wavesplatform/gowaves/pkg/crypto/bls" "github.com/wavesplatform/gowaves/pkg/proto" ) @@ -51,6 +52,28 @@ func (a *EmbeddedWalletImpl) FindPublicKeyByAddress(address proto.WavesAddress, return crypto.PublicKey{}, ErrPublicKeyNotFound } +func (a *EmbeddedWalletImpl) BlsPairByWavesPK(publicKey crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) { + seeds := a.seeder.AccountSeeds() + for _, s := range seeds { + _, publicKeyRetrieved, err := crypto.GenerateKeyPair(s) + if err != nil { + return bls.SecretKey{}, bls.PublicKey{}, err + } + if publicKeyRetrieved == publicKey { + secretKeyBls, genErr := bls.GenerateSecretKey(s) + if genErr != nil { + return bls.SecretKey{}, bls.PublicKey{}, genErr + } + publicKeyBls, retrieveErr := secretKeyBls.PublicKey() + if retrieveErr != nil { + return bls.SecretKey{}, bls.PublicKey{}, retrieveErr + } + return secretKeyBls, publicKeyBls, nil + } + } + return bls.SecretKey{}, bls.PublicKey{}, ErrPublicKeyNotFound +} + func (a *EmbeddedWalletImpl) Load(password []byte) error { bts, err := a.loader.Load() if err != nil { diff --git a/pkg/wallet/stub.go b/pkg/wallet/stub.go index 1fc6889d74..b402b3b068 100644 --- a/pkg/wallet/stub.go +++ b/pkg/wallet/stub.go @@ -2,6 +2,7 @@ package wallet import ( "github.com/wavesplatform/gowaves/pkg/crypto" + "github.com/wavesplatform/gowaves/pkg/crypto/bls" "github.com/wavesplatform/gowaves/pkg/proto" ) @@ -17,6 +18,10 @@ func (s Stub) FindPublicKeyByAddress(_ proto.WavesAddress, _ proto.Scheme) (cryp panic("Stub.FindPublicKeyByAddress: Unsupported operation") } +func (s Stub) BlsPairByWavesPK(_ crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) { + panic("Stub.BlsPairByWavesPK: Unsupported operation") +} + func (s Stub) Load(_ []byte) error { panic("Stub.Load: Unsupported operation") } From 282bf9ff4439e0458e377814429379670c1a72ec Mon Sep 17 00:00:00 2001 From: esuwu Date: Wed, 3 Dec 2025 09:50:25 +0100 Subject: [PATCH 08/17] Added finalization validation --- pkg/api/node_api.go | 2 +- pkg/miner/endorsementpool/endorsement_pool.go | 6 +- pkg/proto/finalization.go | 6 + pkg/state/appender.go | 233 ++++++++++++------ pkg/state/commitments.go | 52 +++- pkg/state/finalization.go | 82 +++--- pkg/state/keys.go | 9 + pkg/state/state.go | 58 ++--- pkg/types/types.go | 2 +- pkg/wallet/embedded_wallet.go | 2 +- pkg/wallet/stub.go | 6 +- 11 files changed, 289 insertions(+), 169 deletions(-) diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index db7222def5..a56312a27d 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -1011,7 +1011,7 @@ func (a *NodeApi) TransactionsSignCommit(w http.ResponseWriter, r *http.Request) return errors.Wrap(err, "failed to find key pair by address") } - blsSecretKey, blsPublicKey, err := a.app.services.Wallet.BlsPairByWavesPK(senderPK) + blsSecretKey, blsPublicKey, err := a.app.services.Wallet.BLSPairByWavesPK(senderPK) if err != nil { return errors.Wrap(err, "failed to find endorser public key by generator public key") } diff --git a/pkg/miner/endorsementpool/endorsement_pool.go b/pkg/miner/endorsementpool/endorsement_pool.go index b93ecb2038..6fd70a404a 100644 --- a/pkg/miner/endorsementpool/endorsement_pool.go +++ b/pkg/miner/endorsementpool/endorsement_pool.go @@ -210,9 +210,9 @@ func (p *EndorsementPool) Verify() (bool, error) { for _, it := range p.h { sigs = append(sigs, it.eb.Signature) pks = append(pks, it.endorserPK) - nextMsg, err := it.eb.EndorsementMessage() - if err != nil { - return false, err + nextMsg, msgErr := it.eb.EndorsementMessage() + if msgErr != nil { + return false, msgErr } if bytes.Equal(nextMsg, msg) { return false, errors.New("failed to verify endorsements: inconsistent endorsement messages") diff --git a/pkg/proto/finalization.go b/pkg/proto/finalization.go index 281d24778d..761bacf9f5 100644 --- a/pkg/proto/finalization.go +++ b/pkg/proto/finalization.go @@ -143,3 +143,9 @@ func (f *FinalizationVoting) ToProtobuf() (*g.FinalizationVoting, error) { } return &finalizationVoting, nil } + +func CalculateLastFinalizedHeight(currentHeight Height) Height { + const genesisHeight = 1 + const maxRollbackDeltaHeight = 100 + return max(genesisHeight, currentHeight-maxRollbackDeltaHeight) +} diff --git a/pkg/state/appender.go b/pkg/state/appender.go index a00d27c158..af85737f7b 100644 --- a/pkg/state/appender.go +++ b/pkg/state/appender.go @@ -4,6 +4,7 @@ import ( stderrs "errors" "fmt" "github.com/wavesplatform/gowaves/pkg/crypto/bls" + "github.com/wavesplatform/gowaves/pkg/util/common" "log/slog" "github.com/ccoveille/go-safecast/v2" @@ -791,85 +792,181 @@ func (a *txAppender) appendFixSnapshots(fixSnapshots []proto.AtomicSnapshot, blo return nil } -func (a *txAppender) isLastBlockFinalized( - finalizationVoting *proto.FinalizationVoting, - block *proto.BlockHeader, - height proto.Height, -) (bool, error) { - - if finalizationVoting == nil { - return false, nil +func (a *txAppender) votingFinalization(endorsers []proto.WavesAddress, height proto.Height, + allGenerators []proto.WavesAddress) (bool, error) { + var totalGeneratingBalance uint64 + var endorsersGeneratingBalance uint64 + for _, gen := range allGenerators { + balance, err := a.stor.balances.generatingBalance(gen.ID(), height) + if err != nil { + return false, err + } + totalGeneratingBalance, err = common.AddInt(totalGeneratingBalance, balance) + if err != nil { + return false, errors.Wrap(err, "totalGeneratingBalance overflow") + } } - // TODO what if it's empty? - lastFinalized, err := a.stor.finalizations.newest() - if err != nil { - return false, err + for _, endorser := range endorsers { + balance, err := a.stor.balances.generatingBalance(endorser.ID(), height) + if err != nil { + return false, err + } + endorsersGeneratingBalance, err = common.AddInt(endorsersGeneratingBalance, balance) + if err != nil { + return false, errors.Wrap(err, "endorsersGeneratingBalance overflow") + } } - msg, err := proto.EndorsementMessage( - lastFinalized.Block.ID, - block.ID, - lastFinalized.FinalizedBlockHeight, - ) - if err != nil { - return false, fmt.Errorf("failed to build endorsement message: %w", err) + if totalGeneratingBalance == 0 { + return false, nil + } + if endorsersGeneratingBalance == 0 { + return false, nil } - // 2. Восстанавливаем endorser PK по индексам - activationHeight, err := a.stor.features.activationHeight(int16(settings.DeterministicFinality)) - if err != nil { - return false, fmt.Errorf("failed to load activation height: %w", err) + // If endorsersBalance >= 2/3 totalGeneratingBalance + if endorsersGeneratingBalance*3 >= totalGeneratingBalance*2 { + return true, nil } + return false, nil +} - periodStart, err := CurrentGenerationPeriodStart( - activationHeight, height, a.settings.GenerationPeriod, - ) - if err != nil { - return false, fmt.Errorf("failed to compute period start: %w", err) +func (a *txAppender) loadLastFinalizedHeight( + height proto.Height, + block *proto.BlockHeader, + finalityActivated bool, +) (proto.Height, error) { + h, err := a.stor.finalizations.newest() + if err == nil { + return h, nil + } + if !errors.Is(err, ErrNoFinalization) && !errors.Is(err, ErrNoFinalizationHistory) { + return 0, err + } + // No finalization in history — calculate it and, if finality is activated - initialize it. + initH := proto.CalculateLastFinalizedHeight(height) + if finalityActivated { + if storErr := a.stor.finalizations.store(initH, block.BlockID()); storErr != nil { + return 0, storErr + } } + return initH, nil +} - endorsersPK := make([]bls.PublicKey, 0, len(finalizationVoting.EndorserIndexes)) - for _, idx := range finalizationVoting.EndorserIndexes { - pk, err := a.stor.commitments.FindEndorserPKsByIndexes(periodStart, int(idx)) +func (a *txAppender) loadEndorsersPK( + fv *proto.FinalizationVoting, + periodStart uint32, +) ([]bls.PublicKey, error) { + endorsersPK := make([]bls.PublicKey, 0, len(fv.EndorserIndexes)) + for _, idx := range fv.EndorserIndexes { + pk, err := a.stor.commitments.FindEndorserPKByIndex(periodStart, int(idx)) if err != nil { - return false, fmt.Errorf("failed to find endorser PK by index %d: %w", idx, err) + return nil, fmt.Errorf("failed to find endorser PK by index %d: %w", idx, err) } endorsersPK = append(endorsersPK, pk) } - if len(endorsersPK) == 0 { - return false, fmt.Errorf("finalization has no endorsers") + return nil, fmt.Errorf("finalization has no endorsers") + } + return endorsersPK, nil +} + +func (a *txAppender) mapEndorsersToAddresses( + endorsersPK []bls.PublicKey, + periodStart uint32, +) ([]proto.WavesAddress, error) { + addrs := make([]proto.WavesAddress, 0, len(endorsersPK)) + for _, endPK := range endorsersPK { + gpk, err := a.stor.commitments.FindGeneratorPKByEndorserPK(periodStart, endPK) + if err != nil { + return nil, fmt.Errorf("failed to map endorser PK to generator PK: %w", err) + } + addr := proto.MustAddressFromPublicKey(a.settings.AddressSchemeCharacter, gpk) + addrs = append(addrs, addr) } + return addrs, nil +} - aggBytes := finalizationVoting.AggregatedEndorsementSignature[:] - ok := bls.VerifyAggregate(endorsersPK, msg, aggBytes) - if !ok { - return false, fmt.Errorf("invalid aggregated BLS signature") - } - // TODO finish this - // 4. Конвертируем endorserPK -> generatorPK -> addresses - //endorserAddresses := make([]proto.WavesAddress, 0, len(endorsersPK)) - //for _, endPK := range endorsersPK { - // genPK, err := a.stor.FindGeneratorPKByEndorserPK(periodStart, endPK) - // if err != nil { - // return false, fmt.Errorf("failed to map endorser PK to generator PK: %w", err) - // } - // addr := proto.MustAddressFromPublicKey(a.stor.scheme, genPK) - // endorserAddresses = append(endorserAddresses, addr) - //} - // - //// 5. Берём всех commited generators - //generators, err := a.stor.CommittedGenerators(periodStart) - //if err != nil { - // return false, fmt.Errorf("failed to load committed generators: %w", err) - //} - // - //// Проверяем >= 2/3 голосов - //canFinalize, err := a.stor.CalculateVotingFinalization(endorserAddresses, height, generators) - //if err != nil { - // return false, fmt.Errorf("failed to calculate 2/3 voting: %w", err) - //} - var canFinalize bool - return canFinalize, nil +func (a *txAppender) verifyFinalizationSignature( + fv *proto.FinalizationVoting, + msg []byte, + endorsersPK []bls.PublicKey, +) error { + aggBytes := fv.AggregatedEndorsementSignature[:] + if !bls.VerifyAggregate(endorsersPK, msg, aggBytes) { + return fmt.Errorf("invalid aggregated BLS signature") + } + return nil +} + +func (a *txAppender) calcPeriodStart(height proto.Height) (uint32, error) { + activation, err := a.stor.features.activationHeight(int16(settings.DeterministicFinality)) + if err != nil { + return 0, fmt.Errorf("failed to load activation height: %w", err) + } + return CurrentGenerationPeriodStart(activation, height, a.settings.GenerationPeriod) +} +func (a *txAppender) updateFinalization( + finalizationVoting *proto.FinalizationVoting, + block *proto.BlockHeader, + height proto.Height, +) error { + if finalizationVoting == nil { + return nil + } + finalityActivated, err := a.stor.features.newestIsActivated(int16(settings.DeterministicFinality)) + if err != nil { + return err + } + lastFinalizedHeight, loadHeightErr := a.loadLastFinalizedHeight( // Or init. + height, block, finalityActivated, + ) + if loadHeightErr != nil { + return loadHeightErr + } + lastFinalizedBlockID, blockPullErr := a.rw.blockIDByHeight(lastFinalizedHeight) + if blockPullErr != nil { + return fmt.Errorf("failed to load last finalized block ID: %w", blockPullErr) + } + msg, msgErr := proto.EndorsementMessage( + lastFinalizedBlockID, + block.Parent, + lastFinalizedHeight, + ) + if msgErr != nil { + return fmt.Errorf("failed to build endorsement message: %w", msgErr) + } + periodStart, caclErr := a.calcPeriodStart(height) + if caclErr != nil { + return caclErr + } + endorsersPK, loadErr := a.loadEndorsersPK(finalizationVoting, periodStart) + if loadErr != nil { + return loadErr + } + if verifyErr := a.verifyFinalizationSignature(finalizationVoting, msg, endorsersPK); verifyErr != nil { + return verifyErr + } + // 6. Map PKs -> addresses + endorserAddresses, mapErr := a.mapEndorsersToAddresses(endorsersPK, periodStart) + if mapErr != nil { + return mapErr + } + // 7. Get committed generators + generators, pullErr := a.stor.commitments.CommittedGenerators(periodStart, a.settings.AddressSchemeCharacter) + if pullErr != nil { + return fmt.Errorf("failed to load committed generators: %w", pullErr) + } + // 8. Check ≥ 2/3 votes + canFinalize, votingErr := a.votingFinalization(endorserAddresses, height, generators) + if votingErr != nil { + return fmt.Errorf("failed to calculate 2/3 voting: %w", votingErr) + } + if canFinalize { + if storErr := a.stor.finalizations.store(height, block.BlockID()); storErr != nil { + return storErr + } + } + return nil } func (a *txAppender) appendBlock(params *appendBlockParams) error { @@ -887,16 +984,10 @@ func (a *txAppender) appendBlock(params *appendBlockParams) error { checkerInfo.parentTimestamp = params.parent.Timestamp } - isFinalized, err := a.isLastBlockFinalized(params.block.FinalizationVoting, params.block, params.blockchainHeight) + err = a.updateFinalization(params.block.FinalizationVoting, params.block, params.blockchainHeight) if err != nil { return err } - if isFinalized { - err := a.stor.finalizations.store(*params.block, params.blockchainHeight, params.block.BlockID()) - if err != nil { - return err - } - } snapshotApplierInfo := newBlockSnapshotsApplierInfo(checkerInfo, a.settings.AddressSchemeCharacter) cleanup := a.txHandler.sa.SetApplierInfo(snapshotApplierInfo) diff --git a/pkg/state/commitments.go b/pkg/state/commitments.go index 3bafc6b5bc..b184e57fb3 100644 --- a/pkg/state/commitments.go +++ b/pkg/state/commitments.go @@ -4,6 +4,9 @@ import ( "bytes" "fmt" + "github.com/pkg/errors" + "github.com/wavesplatform/gowaves/pkg/keyvalue" + "github.com/fxamacker/cbor/v2" "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/crypto/bls" @@ -191,9 +194,9 @@ func (c *commitments) newestSize(periodStart uint32) (int, error) { return len(rec.Commitments), nil } -// FindEndorserPKsByIndexes returns BLS endorser public keys using +// FindEndorserPKByIndex returns BLS endorser public keys using // commitment indexes stored in FinalizationVoting.EndorserIndexes. -func (c *commitments) FindEndorserPKsByIndexes( +func (c *commitments) FindEndorserPKByIndex( periodStart uint32, index int, ) (bls.PublicKey, error) { var empty bls.PublicKey @@ -207,8 +210,8 @@ func (c *commitments) FindEndorserPKsByIndexes( } var rec commitmentsRecord - if err := rec.unmarshalBinary(data); err != nil { - return empty, fmt.Errorf("failed to unmarshal commitments: %w", err) + if unmarshalErr := rec.unmarshalBinary(data); unmarshalErr != nil { + return empty, fmt.Errorf("failed to unmarshal commitments: %w", unmarshalErr) } if index < 0 || index >= len(rec.Commitments) { @@ -217,3 +220,44 @@ func (c *commitments) FindEndorserPKsByIndexes( return rec.Commitments[index].EndorserPK, nil } + +func (c *commitments) FindGeneratorPKByEndorserPK(periodStart uint32, + endorserPK bls.PublicKey) (crypto.PublicKey, error) { + key := commitmentKey{periodStart: periodStart} + data, err := c.hs.newestTopEntryData(key.bytes()) + if err != nil { + if errors.Is(err, keyvalue.ErrNotFound) { + return crypto.PublicKey{}, errors.Errorf("no commitments found for period %d, %v", periodStart, err) + } + return crypto.PublicKey{}, errors.Errorf("failed to retrieve commitments record: %v", err) + } + + var rec commitmentsRecord + if umErr := rec.unmarshalBinary(data); umErr != nil { + return crypto.PublicKey{}, fmt.Errorf("failed to unmarshal commitments record: %w", umErr) + } + + endPKb := endorserPK[:] + for _, cm := range rec.Commitments { + if bytes.Equal(endPKb, cm.EndorserPK[:]) { + return cm.GeneratorPK, nil + } + } + return crypto.PublicKey{}, fmt.Errorf("endorser public key not found in commitments for period %d", periodStart) +} + +func (c *commitments) CommittedGenerators(periodStart uint32, scheme proto.Scheme) ([]proto.WavesAddress, error) { + pks, err := c.newestGenerators(periodStart) + if err != nil { + return nil, err + } + addresses := make([]proto.WavesAddress, len(pks)) + for i, pk := range pks { + addr, cnvrtErr := proto.NewAddressFromPublicKey(scheme, pk) + if cnvrtErr != nil { + return nil, cnvrtErr + } + addresses[i] = addr + } + return addresses, nil +} diff --git a/pkg/state/finalization.go b/pkg/state/finalization.go index cc62a880f7..5853d55c69 100644 --- a/pkg/state/finalization.go +++ b/pkg/state/finalization.go @@ -8,29 +8,21 @@ import ( "github.com/wavesplatform/gowaves/pkg/proto" ) -const finalizationKey = "finalization" - -var ErrNoFinalization = errors.New("no finalized blocks recorded") +var ErrNoFinalization = errors.New("no finalized block recorded") var ErrNoFinalizationHistory = errors.New("no finalization in history") -type finalizationItem struct { - Block proto.BlockHeader `cbor:"0,keyasint,omitempty"` - FinalizedBlockHeight proto.Height `cbor:"1,keyasint,omitempty"` -} - +// finalizationRecord stores only last finalized height. type finalizationRecord struct { - Records []finalizationItem `cbor:"0,keyasint,omitempty"` + FinalizedBlockHeight proto.Height `cbor:"0,keyasint,omitempty"` } -func (fr *finalizationRecord) append(block proto.BlockHeader, finalizedBlockHeight proto.Height) { - fr.Records = append(fr.Records, finalizationItem{ - Block: block, - FinalizedBlockHeight: finalizedBlockHeight, - }) +func (fr *finalizationRecord) marshalBinary() ([]byte, error) { + return cbor.Marshal(fr) } -func (fr *finalizationRecord) marshalBinary() ([]byte, error) { return cbor.Marshal(fr) } -func (fr *finalizationRecord) unmarshalBinary(data []byte) error { return cbor.Unmarshal(data, fr) } +func (fr *finalizationRecord) unmarshalBinary(data []byte) error { + return cbor.Unmarshal(data, fr) +} type finalizations struct { hs *historyStorage @@ -40,46 +32,48 @@ func newFinalizations(hs *historyStorage) *finalizations { return &finalizations{hs: hs} } -func (f *finalizations) store(block proto.BlockHeader, finalizedBlockHeight proto.Height, - currentBlockID proto.BlockID) error { - key := []byte(finalizationKey) - data, err := f.hs.newestTopEntryData(key) - if err != nil && !isNotFoundInHistoryOrDBErr(err) { - return fmt.Errorf("failed to retrieve finalization record: %w", err) - } - var rec finalizationRecord - if len(data) != 0 { - if umErr := rec.unmarshalBinary(data); umErr != nil { - return fmt.Errorf("failed to unmarshal finalization record: %w", umErr) - } +// store replaces existing finalization with a new height. +func (f *finalizations) store( + finalizedBlockHeight proto.Height, + currentBlockID proto.BlockID, +) error { + key := finalizationKey{} + + rec := finalizationRecord{ + FinalizedBlockHeight: finalizedBlockHeight, } - rec.append(block, finalizedBlockHeight) - newData, mErr := rec.marshalBinary() - if mErr != nil { - return fmt.Errorf("failed to marshal finalization record: %w", mErr) + + newData, err := rec.marshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal finalization record: %w", err) } - if addErr := f.hs.addNewEntry(finalization, key, newData, currentBlockID); addErr != nil { + + if addErr := f.hs.addNewEntry(finalization, key.bytes(), newData, currentBlockID); addErr != nil { return fmt.Errorf("failed to add finalization record: %w", addErr) } + return nil } -// newest returns the last finalized block (if exists). -func (f *finalizations) newest() (*finalizationItem, error) { - key := []byte(finalizationKey) - data, err := f.hs.newestTopEntryData(key) +// newest returns the last finalized height. +func (f *finalizations) newest() (proto.Height, error) { + key := finalizationKey{} + data, err := f.hs.newestTopEntryData(key.bytes()) if err != nil { if isNotFoundInHistoryOrDBErr(err) { - return nil, ErrNoFinalizationHistory + return 0, ErrNoFinalizationHistory } - return nil, fmt.Errorf("failed to retrieve finalization record: %w", err) + return 0, fmt.Errorf("failed to retrieve finalization record: %w", err) } + var rec finalizationRecord - if unmrshhlErr := rec.unmarshalBinary(data); unmrshhlErr != nil { - return nil, fmt.Errorf("failed to unmarshal finalization record: %w", unmrshhlErr) + if unmarshalErr := rec.unmarshalBinary(data); unmarshalErr != nil { + return 0, fmt.Errorf("failed to unmarshal finalization record: %w", unmarshalErr) } - if len(rec.Records) == 0 { - return nil, ErrNoFinalization + + if rec.FinalizedBlockHeight == 0 { + return 0, ErrNoFinalization } - return &rec.Records[len(rec.Records)-1], nil + + return rec.FinalizedBlockHeight, nil } diff --git a/pkg/state/keys.go b/pkg/state/keys.go index 9a0fbffe84..b176740b79 100644 --- a/pkg/state/keys.go +++ b/pkg/state/keys.go @@ -137,6 +137,8 @@ const ( challengedAddressKeyPrefix // Key to store and retrieve generator's commitments for a specific generation period. commitmentKeyPrefix + // Key to store and retrieve last finalization record. + finalizationKeyPrefix ) var ( @@ -774,3 +776,10 @@ func (k *commitmentKey) bytes() []byte { binary.BigEndian.PutUint32(buf[1:], k.periodStart) return buf } + +type finalizationKey struct{} + +func (k finalizationKey) bytes() []byte { + // Можно использовать один байт-префикс, как commitments + return []byte{finalizationKeyPrefix} +} diff --git a/pkg/state/state.go b/pkg/state/state.go index bdc78c11aa..f06a97ef90 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -3315,65 +3315,41 @@ func (s *stateManager) CalculateVotingFinalization(endorsers []proto.WavesAddres // FindEndorserPKByIndex retrieves the BLS endorser public key by its index // in the commitments list for the given period. func (s *stateManager) FindEndorserPKByIndex(periodStart uint32, index int) (bls.PublicKey, error) { - return s.stor.commitments.FindEndorserPKsByIndexes(periodStart, index) + return s.stor.commitments.FindEndorserPKByIndex(periodStart, index) } // FindGeneratorPKByEndorserPK finds the generator's Waves public key corresponding // to the given BLS endorser public key in the commitments record for the given period. func (s *stateManager) FindGeneratorPKByEndorserPK(periodStart uint32, endorserPK bls.PublicKey) (crypto.PublicKey, error) { - key := commitmentKey{periodStart: periodStart} - data, err := s.stor.commitments.hs.newestTopEntryData(key.bytes()) - if err != nil { - if errors.Is(err, keyvalue.ErrNotFound) { - return crypto.PublicKey{}, errors.Errorf("no commitments found for period %d, %v", periodStart, err) - } - return crypto.PublicKey{}, errors.Errorf("failed to retrieve commitments record: %v", err) - } - - var rec commitmentsRecord - if umErr := rec.unmarshalBinary(data); umErr != nil { - return crypto.PublicKey{}, fmt.Errorf("failed to unmarshal commitments record: %w", umErr) - } - - endPKb := endorserPK[:] - for _, cm := range rec.Commitments { - if bytes.Equal(endPKb, cm.EndorserPK[:]) { - return cm.GeneratorPK, nil - } - } - return crypto.PublicKey{}, fmt.Errorf("endorser public key not found in commitments for period %d", periodStart) + return s.stor.commitments.FindGeneratorPKByEndorserPK(periodStart, endorserPK) } // CommittedGenerators returns the list of Waves addresses of committed generators. func (s *stateManager) CommittedGenerators(periodStart uint32) ([]proto.WavesAddress, error) { - pks, err := s.stor.commitments.newestGenerators(periodStart) - if err != nil { - return nil, err - } - addresses := make([]proto.WavesAddress, len(pks)) - for i, pk := range pks { - addr, cnvrtErr := proto.NewAddressFromPublicKey(s.settings.AddressSchemeCharacter, pk) - if cnvrtErr != nil { - return nil, cnvrtErr - } - addresses[i] = addr - } - return addresses, nil + return s.stor.commitments.CommittedGenerators(periodStart, s.settings.AddressSchemeCharacter) } func (s *stateManager) LastFinalizedHeight() (proto.Height, error) { - item, err := s.stor.finalizations.newest() - if err != nil || item == nil { + height, err := s.stor.finalizations.newest() + if err != nil { return 0, err } - return item.FinalizedBlockHeight, nil + return height, nil } func (s *stateManager) LastFinalizedBlock() (*proto.BlockHeader, error) { - item, err := s.stor.finalizations.newest() - if err != nil || item == nil { + height, err := s.stor.finalizations.newest() + if err != nil { + return nil, err + } + blockID, err := s.rw.blockIDByHeight(height) + if err != nil { + return nil, err + } + header, err := s.rw.readBlockHeader(blockID) + if err != nil { return nil, err } - return &item.Block, nil + return header, nil } diff --git a/pkg/types/types.go b/pkg/types/types.go index e5b0e6b4f4..d425dcc723 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -223,7 +223,7 @@ type MinerConsensus interface { type EmbeddedWallet interface { SignTransactionWith(pk crypto.PublicKey, tx proto.Transaction) error FindPublicKeyByAddress(address proto.WavesAddress, scheme proto.Scheme) (crypto.PublicKey, error) - BlsPairByWavesPK(publicKey crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) + BLSPairByWavesPK(publicKey crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) Load(password []byte) error AccountSeeds() [][]byte } diff --git a/pkg/wallet/embedded_wallet.go b/pkg/wallet/embedded_wallet.go index fdd529aeb8..3caa094a27 100644 --- a/pkg/wallet/embedded_wallet.go +++ b/pkg/wallet/embedded_wallet.go @@ -52,7 +52,7 @@ func (a *EmbeddedWalletImpl) FindPublicKeyByAddress(address proto.WavesAddress, return crypto.PublicKey{}, ErrPublicKeyNotFound } -func (a *EmbeddedWalletImpl) BlsPairByWavesPK(publicKey crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) { +func (a *EmbeddedWalletImpl) BLSPairByWavesPK(publicKey crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) { seeds := a.seeder.AccountSeeds() for _, s := range seeds { _, publicKeyRetrieved, err := crypto.GenerateKeyPair(s) diff --git a/pkg/wallet/stub.go b/pkg/wallet/stub.go index b402b3b068..72e0b00197 100644 --- a/pkg/wallet/stub.go +++ b/pkg/wallet/stub.go @@ -11,15 +11,15 @@ type Stub struct { } func (s Stub) SignTransactionWith(_ crypto.PublicKey, _ proto.Transaction) error { - panic("Stub.SignTransactionWith: Unsopported operation") + panic("Stub.SignTransactionWith: Unsupported operation") } func (s Stub) FindPublicKeyByAddress(_ proto.WavesAddress, _ proto.Scheme) (crypto.PublicKey, error) { panic("Stub.FindPublicKeyByAddress: Unsupported operation") } -func (s Stub) BlsPairByWavesPK(_ crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) { - panic("Stub.BlsPairByWavesPK: Unsupported operation") +func (s Stub) BLSPairByWavesPK(_ crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) { + panic("Stub.BLSPairByWavesPK: Unsupported operation") } func (s Stub) Load(_ []byte) error { From cd82ba34eea95fa8eaebc2b11cb0e082c16d80c1 Mon Sep 17 00:00:00 2001 From: esuwu Date: Wed, 3 Dec 2025 10:12:44 +0100 Subject: [PATCH 09/17] Added a finalization processor for tx appender --- pkg/state/appender.go | 392 +++++++++++++++++++++++------------------- 1 file changed, 214 insertions(+), 178 deletions(-) diff --git a/pkg/state/appender.go b/pkg/state/appender.go index af85737f7b..188b28c372 100644 --- a/pkg/state/appender.go +++ b/pkg/state/appender.go @@ -62,6 +62,8 @@ type txAppender struct { buildApiData bool bUpdatesPluginInfo *proto.BlockchainUpdatesPluginInfo + + finalizer *finalizationProcessor } func newTxAppender( @@ -105,6 +107,7 @@ func newTxAppender( } ia := newInvokeApplier(state, sc, txHandler, stor, settings, blockDiffer, diffStorInvoke, diffApplier) ethKindResolver := proto.NewEthereumTransactionKindResolver(state, settings.AddressSchemeCharacter) + finalizer := newFinalizationProcessor(stor, rw, settings) return &txAppender{ sc: sc, ia: ia, @@ -122,6 +125,7 @@ func newTxAppender( buildApiData: buildAPIData, ethTxKindResolver: ethKindResolver, bUpdatesPluginInfo: bUpdatesPluginInfo, + finalizer: finalizer, }, nil } @@ -792,183 +796,6 @@ func (a *txAppender) appendFixSnapshots(fixSnapshots []proto.AtomicSnapshot, blo return nil } -func (a *txAppender) votingFinalization(endorsers []proto.WavesAddress, height proto.Height, - allGenerators []proto.WavesAddress) (bool, error) { - var totalGeneratingBalance uint64 - var endorsersGeneratingBalance uint64 - for _, gen := range allGenerators { - balance, err := a.stor.balances.generatingBalance(gen.ID(), height) - if err != nil { - return false, err - } - totalGeneratingBalance, err = common.AddInt(totalGeneratingBalance, balance) - if err != nil { - return false, errors.Wrap(err, "totalGeneratingBalance overflow") - } - } - for _, endorser := range endorsers { - balance, err := a.stor.balances.generatingBalance(endorser.ID(), height) - if err != nil { - return false, err - } - endorsersGeneratingBalance, err = common.AddInt(endorsersGeneratingBalance, balance) - if err != nil { - return false, errors.Wrap(err, "endorsersGeneratingBalance overflow") - } - } - if totalGeneratingBalance == 0 { - return false, nil - } - if endorsersGeneratingBalance == 0 { - return false, nil - } - - // If endorsersBalance >= 2/3 totalGeneratingBalance - if endorsersGeneratingBalance*3 >= totalGeneratingBalance*2 { - return true, nil - } - return false, nil -} - -func (a *txAppender) loadLastFinalizedHeight( - height proto.Height, - block *proto.BlockHeader, - finalityActivated bool, -) (proto.Height, error) { - h, err := a.stor.finalizations.newest() - if err == nil { - return h, nil - } - if !errors.Is(err, ErrNoFinalization) && !errors.Is(err, ErrNoFinalizationHistory) { - return 0, err - } - // No finalization in history — calculate it and, if finality is activated - initialize it. - initH := proto.CalculateLastFinalizedHeight(height) - if finalityActivated { - if storErr := a.stor.finalizations.store(initH, block.BlockID()); storErr != nil { - return 0, storErr - } - } - return initH, nil -} - -func (a *txAppender) loadEndorsersPK( - fv *proto.FinalizationVoting, - periodStart uint32, -) ([]bls.PublicKey, error) { - endorsersPK := make([]bls.PublicKey, 0, len(fv.EndorserIndexes)) - for _, idx := range fv.EndorserIndexes { - pk, err := a.stor.commitments.FindEndorserPKByIndex(periodStart, int(idx)) - if err != nil { - return nil, fmt.Errorf("failed to find endorser PK by index %d: %w", idx, err) - } - endorsersPK = append(endorsersPK, pk) - } - if len(endorsersPK) == 0 { - return nil, fmt.Errorf("finalization has no endorsers") - } - return endorsersPK, nil -} - -func (a *txAppender) mapEndorsersToAddresses( - endorsersPK []bls.PublicKey, - periodStart uint32, -) ([]proto.WavesAddress, error) { - addrs := make([]proto.WavesAddress, 0, len(endorsersPK)) - for _, endPK := range endorsersPK { - gpk, err := a.stor.commitments.FindGeneratorPKByEndorserPK(periodStart, endPK) - if err != nil { - return nil, fmt.Errorf("failed to map endorser PK to generator PK: %w", err) - } - addr := proto.MustAddressFromPublicKey(a.settings.AddressSchemeCharacter, gpk) - addrs = append(addrs, addr) - } - return addrs, nil -} - -func (a *txAppender) verifyFinalizationSignature( - fv *proto.FinalizationVoting, - msg []byte, - endorsersPK []bls.PublicKey, -) error { - aggBytes := fv.AggregatedEndorsementSignature[:] - if !bls.VerifyAggregate(endorsersPK, msg, aggBytes) { - return fmt.Errorf("invalid aggregated BLS signature") - } - return nil -} - -func (a *txAppender) calcPeriodStart(height proto.Height) (uint32, error) { - activation, err := a.stor.features.activationHeight(int16(settings.DeterministicFinality)) - if err != nil { - return 0, fmt.Errorf("failed to load activation height: %w", err) - } - return CurrentGenerationPeriodStart(activation, height, a.settings.GenerationPeriod) -} -func (a *txAppender) updateFinalization( - finalizationVoting *proto.FinalizationVoting, - block *proto.BlockHeader, - height proto.Height, -) error { - if finalizationVoting == nil { - return nil - } - finalityActivated, err := a.stor.features.newestIsActivated(int16(settings.DeterministicFinality)) - if err != nil { - return err - } - lastFinalizedHeight, loadHeightErr := a.loadLastFinalizedHeight( // Or init. - height, block, finalityActivated, - ) - if loadHeightErr != nil { - return loadHeightErr - } - lastFinalizedBlockID, blockPullErr := a.rw.blockIDByHeight(lastFinalizedHeight) - if blockPullErr != nil { - return fmt.Errorf("failed to load last finalized block ID: %w", blockPullErr) - } - msg, msgErr := proto.EndorsementMessage( - lastFinalizedBlockID, - block.Parent, - lastFinalizedHeight, - ) - if msgErr != nil { - return fmt.Errorf("failed to build endorsement message: %w", msgErr) - } - periodStart, caclErr := a.calcPeriodStart(height) - if caclErr != nil { - return caclErr - } - endorsersPK, loadErr := a.loadEndorsersPK(finalizationVoting, periodStart) - if loadErr != nil { - return loadErr - } - if verifyErr := a.verifyFinalizationSignature(finalizationVoting, msg, endorsersPK); verifyErr != nil { - return verifyErr - } - // 6. Map PKs -> addresses - endorserAddresses, mapErr := a.mapEndorsersToAddresses(endorsersPK, periodStart) - if mapErr != nil { - return mapErr - } - // 7. Get committed generators - generators, pullErr := a.stor.commitments.CommittedGenerators(periodStart, a.settings.AddressSchemeCharacter) - if pullErr != nil { - return fmt.Errorf("failed to load committed generators: %w", pullErr) - } - // 8. Check ≥ 2/3 votes - canFinalize, votingErr := a.votingFinalization(endorserAddresses, height, generators) - if votingErr != nil { - return fmt.Errorf("failed to calculate 2/3 voting: %w", votingErr) - } - if canFinalize { - if storErr := a.stor.finalizations.store(height, block.BlockID()); storErr != nil { - return storErr - } - } - return nil -} - func (a *txAppender) appendBlock(params *appendBlockParams) error { // Reset block complexity counter. defer func() { @@ -984,7 +811,7 @@ func (a *txAppender) appendBlock(params *appendBlockParams) error { checkerInfo.parentTimestamp = params.parent.Timestamp } - err = a.updateFinalization(params.block.FinalizationVoting, params.block, params.blockchainHeight) + err = a.finalizer.updateFinalization(params.block.FinalizationVoting, params.block, params.blockchainHeight) if err != nil { return err } @@ -1517,3 +1344,212 @@ func (a *txAppender) reset() { a.diffStor.reset() a.blockDiffer.reset() } + +type finalizationProcessor struct { + stor *blockchainEntitiesStorage + rw *blockReadWriter + settings *settings.BlockchainSettings +} + +func newFinalizationProcessor( + stor *blockchainEntitiesStorage, + rw *blockReadWriter, + settings *settings.BlockchainSettings, +) *finalizationProcessor { + return &finalizationProcessor{ + stor: stor, + rw: rw, + settings: settings, + } +} + +func (f *finalizationProcessor) votingFinalization( + endorsers []proto.WavesAddress, + height proto.Height, + allGenerators []proto.WavesAddress, +) (bool, error) { + var totalGeneratingBalance uint64 + var endorsersGeneratingBalance uint64 + + for _, gen := range allGenerators { + balance, err := f.stor.balances.generatingBalance(gen.ID(), height) + if err != nil { + return false, err + } + totalGeneratingBalance, err = common.AddInt(totalGeneratingBalance, balance) + if err != nil { + return false, errors.Wrap(err, "totalGeneratingBalance overflow") + } + } + + for _, endorser := range endorsers { + balance, err := f.stor.balances.generatingBalance(endorser.ID(), height) + if err != nil { + return false, err + } + endorsersGeneratingBalance, err = common.AddInt(endorsersGeneratingBalance, balance) + if err != nil { + return false, errors.Wrap(err, "endorsersGeneratingBalance overflow") + } + } + + if totalGeneratingBalance == 0 { + return false, nil + } + if endorsersGeneratingBalance == 0 { + return false, nil + } + + // endorsersBalance >= 2/3 totalGeneratingBalance + if endorsersGeneratingBalance*3 >= totalGeneratingBalance*2 { + return true, nil + } + return false, nil +} + +func (f *finalizationProcessor) loadLastFinalizedHeight( + height proto.Height, + block *proto.BlockHeader, + finalityActivated bool, +) (proto.Height, error) { + h, err := f.stor.finalizations.newest() + if err == nil { + return h, nil + } + if !errors.Is(err, ErrNoFinalization) && !errors.Is(err, ErrNoFinalizationHistory) { + return 0, err + } + + // No finalization found, calculate it, and, if finality activated - initialize it. + initH := proto.CalculateLastFinalizedHeight(height) + if finalityActivated { + if storErr := f.stor.finalizations.store(initH, block.BlockID()); storErr != nil { + return 0, storErr + } + } + return initH, nil +} + +func (f *finalizationProcessor) loadEndorsersPK( + fv *proto.FinalizationVoting, + periodStart uint32, +) ([]bls.PublicKey, error) { + endorsersPK := make([]bls.PublicKey, 0, len(fv.EndorserIndexes)) + for _, idx := range fv.EndorserIndexes { + pk, err := f.stor.commitments.FindEndorserPKByIndex(periodStart, int(idx)) + if err != nil { + return nil, fmt.Errorf("failed to find endorser PK by index %d: %w", idx, err) + } + endorsersPK = append(endorsersPK, pk) + } + if len(endorsersPK) == 0 { + return nil, fmt.Errorf("finalization has no endorsers") + } + return endorsersPK, nil +} + +func (f *finalizationProcessor) mapEndorsersToAddresses( + endorsersPK []bls.PublicKey, + periodStart uint32, +) ([]proto.WavesAddress, error) { + addrs := make([]proto.WavesAddress, 0, len(endorsersPK)) + for _, endPK := range endorsersPK { + gpk, err := f.stor.commitments.FindGeneratorPKByEndorserPK(periodStart, endPK) + if err != nil { + return nil, fmt.Errorf("failed to map endorser PK to generator PK: %w", err) + } + addr := proto.MustAddressFromPublicKey(f.settings.AddressSchemeCharacter, gpk) + addrs = append(addrs, addr) + } + return addrs, nil +} + +func (f *finalizationProcessor) verifyFinalizationSignature( + fv *proto.FinalizationVoting, + msg []byte, + endorsersPK []bls.PublicKey, +) error { + aggBytes := fv.AggregatedEndorsementSignature[:] + if !bls.VerifyAggregate(endorsersPK, msg, aggBytes) { + return fmt.Errorf("invalid aggregated BLS signature") + } + return nil +} + +func (f *finalizationProcessor) calcPeriodStart(height proto.Height) (uint32, error) { + activation, err := f.stor.features.activationHeight(int16(settings.DeterministicFinality)) + if err != nil { + return 0, fmt.Errorf("failed to load activation height: %w", err) + } + return CurrentGenerationPeriodStart(activation, height, f.settings.GenerationPeriod) +} + +func (f *finalizationProcessor) updateFinalization( + finalizationVoting *proto.FinalizationVoting, + block *proto.BlockHeader, + height proto.Height, +) error { + if finalizationVoting == nil { + return nil + } + + finalityActivated, err := f.stor.features.newestIsActivated(int16(settings.DeterministicFinality)) + if err != nil { + return err + } + + lastFinalizedHeight, err := f.loadLastFinalizedHeight(height, block, finalityActivated) + if err != nil { + return err + } + + lastFinalizedBlockID, err := f.rw.blockIDByHeight(lastFinalizedHeight) + if err != nil { + return fmt.Errorf("failed to load last finalized block ID: %w", err) + } + + msg, err := proto.EndorsementMessage( + lastFinalizedBlockID, + block.Parent, + lastFinalizedHeight, + ) + if err != nil { + return fmt.Errorf("failed to build endorsement message: %w", err) + } + + periodStart, err := f.calcPeriodStart(height) + if err != nil { + return err + } + + endorsersPK, err := f.loadEndorsersPK(finalizationVoting, periodStart) + if err != nil { + return err + } + + if verifyErr := f.verifyFinalizationSignature(finalizationVoting, msg, endorsersPK); verifyErr != nil { + return verifyErr + } + + endorserAddresses, err := f.mapEndorsersToAddresses(endorsersPK, periodStart) + if err != nil { + return err + } + + generators, err := f.stor.commitments.CommittedGenerators(periodStart, f.settings.AddressSchemeCharacter) + if err != nil { + return fmt.Errorf("failed to load committed generators: %w", err) + } + + canFinalize, err := f.votingFinalization(endorserAddresses, height, generators) + if err != nil { + return fmt.Errorf("failed to calculate 2/3 voting: %w", err) + } + + if canFinalize { + if storErr := f.stor.finalizations.store(height, block.BlockID()); storErr != nil { + return storErr + } + } + return nil +} From 40c3d0203088cf603f2abb8ff9f88f888d46e1b5 Mon Sep 17 00:00:00 2001 From: esuwu Date: Fri, 5 Dec 2025 14:29:14 +0100 Subject: [PATCH 10/17] Merged again --- pkg/state/address_transactions_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/state/address_transactions_test.go b/pkg/state/address_transactions_test.go index ac3998c689..3e35353304 100644 --- a/pkg/state/address_transactions_test.go +++ b/pkg/state/address_transactions_test.go @@ -13,12 +13,7 @@ import ( func testIterImpl(t *testing.T, params StateParams) { dataDir := t.TempDir() -<<<<<<< HEAD - st, err := NewState(dataDir, true, params, settings.MustMainNetSettings(), false, - nil) -======= st, err := NewState(t.Context(), dataDir, true, params, settings.MustMainNetSettings(), false, nil) ->>>>>>> add-network-messages require.NoError(t, err) t.Cleanup(func() { From 5dce8daae23982a2f01bb56e28e07ad88195ebae Mon Sep 17 00:00:00 2001 From: esuwu Date: Tue, 9 Dec 2025 14:29:13 +0100 Subject: [PATCH 11/17] Added clients methods --- pkg/client/blocks.go | 41 +++++++++++++++++++++++++++++++++++ pkg/client/client.go | 2 ++ pkg/client/generators.go | 44 ++++++++++++++++++++++++++++++++++++++ pkg/client/transactions.go | 32 +++++++++++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 pkg/client/generators.go diff --git a/pkg/client/blocks.go b/pkg/client/blocks.go index 2a4f096872..d7dde03ae5 100644 --- a/pkg/client/blocks.go +++ b/pkg/client/blocks.go @@ -310,3 +310,44 @@ func (a *Blocks) Address(ctx context.Context, addr proto.WavesAddress, from, to return out, response, nil } + +func (a *Blocks) HeightFinalized(ctx context.Context) (uint64, *Response, error) { + url, err := joinUrl(a.options.BaseUrl, "/blocks/height/finalized") + if err != nil { + return 0, nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) + if err != nil { + return 0, nil, err + } + + var out struct { + Height uint64 `json:"height"` + } + + resp, err := doHTTP(ctx, a.options, req, &out) + if err != nil { + return 0, resp, err + } + + return out.Height, resp, nil +} + +func (a *Blocks) BlockFinalized(ctx context.Context) (*proto.BlockHeader, *Response, error) { + url, err := joinUrl(a.options.BaseUrl, "/blocks/headers/finalized") + if err != nil { + return nil, nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) + if err != nil { + return nil, nil, err + } + var out proto.BlockHeader + resp, err := doHTTP(ctx, a.options, req, &out) + if err != nil { + return nil, resp, err + } + return &out, resp, nil +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 67ce3d5de3..50c406fd77 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -48,6 +48,7 @@ type Client struct { Leasing *Leasing Debug *Debug Blockchain *Blockchain + Generators *Generators } type Response struct { @@ -93,6 +94,7 @@ func NewClient(options ...Options) (*Client, error) { Leasing: NewLeasing(opts), Debug: NewDebug(opts), Blockchain: NewBlockchain(opts), + Generators: NewGenerators(opts), } return c, nil diff --git a/pkg/client/generators.go b/pkg/client/generators.go new file mode 100644 index 0000000000..38546d1759 --- /dev/null +++ b/pkg/client/generators.go @@ -0,0 +1,44 @@ +package client + +import ( + "context" + "fmt" + "net/http" +) + +// Generators is a client wrapper for generator-related API endpoints. +type Generators struct { + options Options +} + +func NewGenerators(options Options) *Generators { + return &Generators{ + options: options, + } +} + +// GeneratorsAtResponse is the expected structure returned by /generators/at/{height}. +type GeneratorsAtResponse struct { + Height uint64 `json:"height"` + Generators []string `json:"generators"` +} + +// CommitmentGeneratorsAt returns the list of committed generators for the given height. +func (a *Generators) CommitmentGeneratorsAt(ctx context.Context, + height uint64) (*GeneratorsAtResponse, *Response, error) { + url, err := joinUrl(a.options.BaseUrl, fmt.Sprintf("/generators/at/%d", height)) + if err != nil { + return nil, nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) + if err != nil { + return nil, nil, err + } + out := new(GeneratorsAtResponse) + resp, err := doHTTP(ctx, a.options, req, out) + if err != nil { + return nil, resp, err + } + + return out, resp, nil +} diff --git a/pkg/client/transactions.go b/pkg/client/transactions.go index 290eb8f5e1..37486050c9 100644 --- a/pkg/client/transactions.go +++ b/pkg/client/transactions.go @@ -184,3 +184,35 @@ func (a *Transactions) Broadcast(ctx context.Context, transaction proto.Transact } return doHTTP(ctx, a.options, req, nil) } + +type SignCommitRequest struct { + Sender string `json:"sender"` + GenerationPeriodStart *uint32 `json:"generationPeriodStart,omitempty"` + Timestamp *int64 `json:"timestamp,omitempty"` +} + +// SignCommit calls POST /transactions/sign and returns the signed CommitToGenerationWithProofs tx. +func (a *Transactions) SignCommit(ctx context.Context, + reqBody *SignCommitRequest) (*proto.CommitToGenerationWithProofs, *Response, error) { + if reqBody == nil { + return nil, nil, fmt.Errorf("empty request body") + } + url, err := joinUrl(a.options.BaseUrl, "/transactions/sign") + if err != nil { + return nil, nil, err + } + bts, err := json.Marshal(reqBody) + if err != nil { + return nil, nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewReader(bts)) + if err != nil { + return nil, nil, err + } + out := new(proto.CommitToGenerationWithProofs) + resp, err := doHTTP(ctx, a.options, req, out) + if err != nil { + return nil, resp, err + } + return out, resp, nil +} From b77abc1073fcb53fb4df68f776c47a8862c110ed Mon Sep 17 00:00:00 2001 From: esuwu Date: Wed, 10 Dec 2025 15:03:04 +0100 Subject: [PATCH 12/17] Added http client itests --- itests/clients/http_client.go | 46 +++++++++++++++++++++++++++++++++++ pkg/api/node_api.go | 3 ++- pkg/client/generators.go | 7 +++--- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/itests/clients/http_client.go b/itests/clients/http_client.go index 48fa70a159..1b43cab8c3 100644 --- a/itests/clients/http_client.go +++ b/itests/clients/http_client.go @@ -2,6 +2,7 @@ package clients import ( "context" + "github.com/wavesplatform/gowaves/pkg/api" "net/http" "testing" "time" @@ -153,3 +154,48 @@ func (c *HTTPClient) RollbackToHeight(t testing.TB, height uint64, returnTxToUtx require.NoErrorf(t, err, "failed to rollback to height on %s node", c.impl.String()) return blockID } + +func (c *HTTPClient) HeightFinalized(t testing.TB) proto.Height { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + h, _, err := c.cli.Blocks.HeightFinalized(ctx) + require.NoErrorf(t, err, "failed to get finalized height from %s node", c.impl.String()) + + return h +} + +func (c *HTTPClient) BlockFinalized(t testing.TB) *proto.BlockHeader { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + header, _, err := c.cli.Blocks.BlockFinalized(ctx) + require.NoErrorf(t, err, "failed to get finalized header from %s node", c.impl.String()) + require.NotNil(t, header, "finalized header is nil from %s node", c.impl.String()) + + return header +} + +func (c *HTTPClient) CommitmentGeneratorsAt(t testing.TB, height proto.Height) []api.GeneratorInfo { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + gens, _, err := c.cli.Generators.CommitmentGeneratorsAt(ctx, height) + require.NoErrorf(t, err, "failed to get generators at height %d from %s node", height, c.impl.String()) + + return gens +} + +func (c *HTTPClient) SignCommit( + t testing.TB, + req *client.SignCommitRequest, +) *proto.CommitToGenerationWithProofs { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + out, _, err := c.cli.Transactions.SignCommit(ctx, req) + require.NoErrorf(t, err, "failed to sign commit transaction on %s node", c.impl.String()) + + require.NotNil(t, out, "empty response from /transactions/sign on %s node", c.impl.String()) + return out +} diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index 99cd75de31..d99950aebe 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -924,7 +924,8 @@ type GeneratorInfo struct { TransactionID string `json:"transactionID"` } -func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { +func (a *NodeApi) st +GeneratorsAt(w http.ResponseWriter, r *http.Request) error { heightStr := chi.URLParam(r, "height") height, err := strconv.ParseUint(heightStr, 10, 64) if err != nil { diff --git a/pkg/client/generators.go b/pkg/client/generators.go index 38546d1759..87183adc0a 100644 --- a/pkg/client/generators.go +++ b/pkg/client/generators.go @@ -3,6 +3,7 @@ package client import ( "context" "fmt" + "github.com/wavesplatform/gowaves/pkg/api" "net/http" ) @@ -25,7 +26,7 @@ type GeneratorsAtResponse struct { // CommitmentGeneratorsAt returns the list of committed generators for the given height. func (a *Generators) CommitmentGeneratorsAt(ctx context.Context, - height uint64) (*GeneratorsAtResponse, *Response, error) { + height uint64) ([]api.GeneratorInfo, *Response, error) { url, err := joinUrl(a.options.BaseUrl, fmt.Sprintf("/generators/at/%d", height)) if err != nil { return nil, nil, err @@ -34,8 +35,8 @@ func (a *Generators) CommitmentGeneratorsAt(ctx context.Context, if err != nil { return nil, nil, err } - out := new(GeneratorsAtResponse) - resp, err := doHTTP(ctx, a.options, req, out) + var out []api.GeneratorInfo + resp, err := doHTTP(ctx, a.options, req, &out) if err != nil { return nil, resp, err } From 6e83e89bf43d1f83b8e00cbfd59de406936229d0 Mon Sep 17 00:00:00 2001 From: esuwu Date: Wed, 10 Dec 2025 15:05:55 +0100 Subject: [PATCH 13/17] Fixed an error --- itests/clients/http_client.go | 2 +- pkg/api/node_api.go | 3 +-- pkg/client/generators.go | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/itests/clients/http_client.go b/itests/clients/http_client.go index 1b43cab8c3..2401fb7dfd 100644 --- a/itests/clients/http_client.go +++ b/itests/clients/http_client.go @@ -2,7 +2,6 @@ package clients import ( "context" - "github.com/wavesplatform/gowaves/pkg/api" "net/http" "testing" "time" @@ -11,6 +10,7 @@ import ( "github.com/wavesplatform/gowaves/itests/config" d "github.com/wavesplatform/gowaves/itests/docker" + "github.com/wavesplatform/gowaves/pkg/api" "github.com/wavesplatform/gowaves/pkg/client" "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/proto" diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index d99950aebe..99cd75de31 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -924,8 +924,7 @@ type GeneratorInfo struct { TransactionID string `json:"transactionID"` } -func (a *NodeApi) st -GeneratorsAt(w http.ResponseWriter, r *http.Request) error { +func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { heightStr := chi.URLParam(r, "height") height, err := strconv.ParseUint(heightStr, 10, 64) if err != nil { diff --git a/pkg/client/generators.go b/pkg/client/generators.go index 87183adc0a..06a82668ee 100644 --- a/pkg/client/generators.go +++ b/pkg/client/generators.go @@ -3,8 +3,9 @@ package client import ( "context" "fmt" - "github.com/wavesplatform/gowaves/pkg/api" "net/http" + + "github.com/wavesplatform/gowaves/pkg/api" ) // Generators is a client wrapper for generator-related API endpoints. From 2bfd983be1144ec4b77b996ee5e45d58604bb05d Mon Sep 17 00:00:00 2001 From: esuwu Date: Wed, 17 Dec 2025 14:28:30 +0100 Subject: [PATCH 14/17] Updated protobuf version --- pkg/grpc/protobuf-schemas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/grpc/protobuf-schemas b/pkg/grpc/protobuf-schemas index f0e15d75b5..759664d25e 160000 --- a/pkg/grpc/protobuf-schemas +++ b/pkg/grpc/protobuf-schemas @@ -1 +1 @@ -Subproject commit f0e15d75b5ecd6a710c334abcb09450f370d034a +Subproject commit 759664d25e3f1c4f7fbb389bea76ce7566e54c94 From 1258e1383d46dd875d4baf241e8cadd1cf56ca17 Mon Sep 17 00:00:00 2001 From: esuwu Date: Tue, 23 Dec 2025 00:22:40 +0100 Subject: [PATCH 15/17] Implemented nickeskov's suggestions --- itests/clients/http_client.go | 4 +- pkg/api/app.go | 20 ++++---- pkg/api/errors.go | 28 ++++------ pkg/api/errors/auth.go | 2 +- pkg/api/errors/basics.go | 74 ++++++++++++++++++++++++++- pkg/api/errors/consts.go | 16 +++--- pkg/api/errors_test.go | 2 +- pkg/api/node_api.go | 34 ++++++++---- pkg/api/peers.go | 5 +- pkg/client/generators.go | 6 +-- pkg/node/blocks_applier/node_mocks.go | 3 +- pkg/state/threadsafe_wrapper.go | 3 +- pkg/wallet/stub.go | 31 ----------- 13 files changed, 140 insertions(+), 88 deletions(-) delete mode 100644 pkg/wallet/stub.go diff --git a/itests/clients/http_client.go b/itests/clients/http_client.go index 2401fb7dfd..dc85d50034 100644 --- a/itests/clients/http_client.go +++ b/itests/clients/http_client.go @@ -10,7 +10,7 @@ import ( "github.com/wavesplatform/gowaves/itests/config" d "github.com/wavesplatform/gowaves/itests/docker" - "github.com/wavesplatform/gowaves/pkg/api" + nodeApi "github.com/wavesplatform/gowaves/pkg/api" "github.com/wavesplatform/gowaves/pkg/client" "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/proto" @@ -176,7 +176,7 @@ func (c *HTTPClient) BlockFinalized(t testing.TB) *proto.BlockHeader { return header } -func (c *HTTPClient) CommitmentGeneratorsAt(t testing.TB, height proto.Height) []api.GeneratorInfo { +func (c *HTTPClient) CommitmentGeneratorsAt(t testing.TB, height proto.Height) []nodeApi.GeneratorInfo { ctx, cancel := context.WithTimeout(context.Background(), c.timeout) defer cancel() diff --git a/pkg/api/app.go b/pkg/api/app.go index 1714e35573..0502ec2341 100644 --- a/pkg/api/app.go +++ b/pkg/api/app.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" + apiErr "github.com/wavesplatform/gowaves/pkg/api/errors" "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/miner/scheduler" "github.com/wavesplatform/gowaves/pkg/node/messages" @@ -63,17 +64,18 @@ func NewApp(apiKey string, scheduler SchedulerEmits, services services.Services, return newApp(apiKey, scheduler, services, nil, cfg) } -func newApp(apiKey string, scheduler SchedulerEmits, services services.Services, settings *appSettings, +func newApp(apiKey string, scheduler SchedulerEmits, services services.Services, appSettings *appSettings, cfg *settings.BlockchainSettings) (*App, error) { - if settings == nil { - settings = defaultAppSettings() + if appSettings == nil { + appSettings = defaultAppSettings() } digest, err := crypto.SecureHash([]byte(apiKey)) if err != nil { return nil, err } - - settings.GenerationPeriod = cfg.GenerationPeriod + if cfg != nil { + appSettings.GenerationPeriod = cfg.GenerationPeriod + } return &App{ hashedApiKey: digest, apiKeyEnabled: len(apiKey) > 0, @@ -82,7 +84,7 @@ func newApp(apiKey string, scheduler SchedulerEmits, services services.Services, utx: services.UtxPool, peers: services.Peers, services: services, - settings: settings, + settings: appSettings, }, nil } @@ -90,17 +92,17 @@ func (a *App) TransactionsBroadcast(ctx context.Context, b []byte) (proto.Transa tt := proto.TransactionTypeVersion{} err := json.Unmarshal(b, &tt) if err != nil { - return nil, wrapToBadRequestError(err) + return nil, apiErr.NewBadRequestError(err) } realType, err := proto.GuessTransactionType(&tt) if err != nil { - return nil, wrapToBadRequestError(err) + return nil, apiErr.NewBadRequestError(err) } err = proto.UnmarshalTransactionFromJSON(b, a.services.Scheme, realType) if err != nil { - return nil, wrapToBadRequestError(err) + return nil, apiErr.NewBadRequestError(err) } respCh := make(chan error, 1) diff --git a/pkg/api/errors.go b/pkg/api/errors.go index bd654dc1e7..c62a1bcfd0 100644 --- a/pkg/api/errors.go +++ b/pkg/api/errors.go @@ -18,20 +18,6 @@ var ( notFound = errors.New("not found") ) -// BadRequestError represents a bad request error. -// Deprecated: don't use this error type in new code. Create a new error type or value in 'pkg/api/errors' package. -type BadRequestError struct { - inner error -} - -func wrapToBadRequestError(err error) *BadRequestError { - return &BadRequestError{inner: err} -} - -func (e *BadRequestError) Error() string { - return e.inner.Error() -} - // AuthError represents an authentication error or problem. // Deprecated: don't use this error type in new code. Create a new error type or value in 'pkg/api/errors' package. type AuthError struct { @@ -62,12 +48,14 @@ func (eh *ErrorHandler) Handle(w http.ResponseWriter, r *http.Request, err error } // target errors var ( - badRequestError *BadRequestError - authError *AuthError - unknownError *apiErrs.UnknownError - apiError apiErrs.ApiError + badRequestError *apiErrs.BadRequestError + authError *AuthError + unknownError *apiErrs.UnknownError + apiError apiErrs.ApiError + unavailableError *apiErrs.UnavailableError // check that all targets implement the error interface - _, _, _, _ = error(badRequestError), error(authError), error(unknownError), error(apiError) + _, _, _, _, _ = error(badRequestError), error(authError), error(unknownError), + error(apiError), error(unavailableError) ) switch { case errors.As(err, &badRequestError): @@ -86,6 +74,8 @@ func (eh *ErrorHandler) Handle(w http.ResponseWriter, r *http.Request, err error eh.sendApiErrJSON(w, r, unknownError) case errors.As(err, &apiError): eh.sendApiErrJSON(w, r, apiError) + case errors.As(err, &unavailableError): + eh.sendApiErrJSON(w, r, unavailableError) default: eh.logger.Error("InternalServerError", slog.String("proto", r.Proto), diff --git a/pkg/api/errors/auth.go b/pkg/api/errors/auth.go index f2ccad9393..2c3d271324 100644 --- a/pkg/api/errors/auth.go +++ b/pkg/api/errors/auth.go @@ -18,7 +18,7 @@ type ( var ( ApiKeyNotValid = &ApiKeyNotValidError{ genericError: genericError{ - ID: ApiKeyNotValidErrorID, + ID: APIKeyNotValidErrorID, HttpCode: http.StatusBadRequest, Message: "Provided API key is not correct", }, diff --git a/pkg/api/errors/basics.go b/pkg/api/errors/basics.go index 20a02d549f..3bbd27c12c 100644 --- a/pkg/api/errors/basics.go +++ b/pkg/api/errors/basics.go @@ -114,7 +114,7 @@ type WrongJsonError struct { func NewWrongJsonError(cause string, validationErrors []error) *WrongJsonError { return &WrongJsonError{ genericError: genericError{ - ID: WrongJsonErrorID, + ID: WrongJSONErrorID, HttpCode: http.StatusBadRequest, Message: "failed to parse json message", }, @@ -122,3 +122,75 @@ func NewWrongJsonError(cause string, validationErrors []error) *WrongJsonError { ValidationErrors: validationErrors, } } + +// UnavailableError UnknownError is a wrapper for any error related to service unavailability. +type UnavailableError struct { + genericError + inner error +} + +func (u *UnavailableError) Unwrap() error { + return u.inner +} + +func (u *UnavailableError) Error() string { + if u.Unwrap() != nil { + return fmt.Sprintf( + "%s; inner error (%T): %s", + u.genericError.Error(), + u.Unwrap(), u.Unwrap().Error(), + ) + } + return u.genericError.Error() +} + +func NewUnavailableError(inner error) *UnavailableError { + return NewUnavailableErrorWithMsg("Service is unavailable", inner) +} + +func NewUnavailableErrorWithMsg(message string, inner error) *UnavailableError { + return &UnavailableError{ + genericError: genericError{ + ID: ServiceUnavailableErrorID, + HttpCode: http.StatusServiceUnavailable, + Message: message, + }, + inner: inner, + } +} + +// BadRequestError is a wrapper for any bad request error. +type BadRequestError struct { + genericError + inner error +} + +func (u *BadRequestError) Unwrap() error { + return u.inner +} + +func (u *BadRequestError) Error() string { + if u.Unwrap() != nil { + return fmt.Sprintf( + "%s; inner error (%T): %s", + u.genericError.Error(), + u.Unwrap(), u.Unwrap().Error(), + ) + } + return u.genericError.Error() +} + +func NewBadRequestError(inner error) *BadRequestError { + return NewBadRequestErrorWithMsg("Bad request", inner) +} + +func NewBadRequestErrorWithMsg(message string, inner error) *BadRequestError { + return &BadRequestError{ + genericError: genericError{ + ID: BadRequestErrorID, + HttpCode: http.StatusBadRequest, + Message: message, + }, + inner: inner, + } +} diff --git a/pkg/api/errors/consts.go b/pkg/api/errors/consts.go index 0a4b829462..99c942257a 100644 --- a/pkg/api/errors/consts.go +++ b/pkg/api/errors/consts.go @@ -1,13 +1,15 @@ package errors const ( - UnknownErrorID ErrorID = 0 - WrongJsonErrorID ErrorID = 1 + UnknownErrorID ErrorID = 0 + WrongJSONErrorID ErrorID = 1 + BadRequestErrorID ErrorID = 2 + ServiceUnavailableErrorID ErrorID = 3 ) // API Auth const ( - ApiKeyNotValidErrorID ApiAuthErrorID = 2 + APIKeyNotValidErrorID ApiAuthErrorID = 4 TooBigArrayAllocationErrorID ApiAuthErrorID = 10 ) @@ -56,10 +58,12 @@ const ( ) var errorNames = map[Identifier]string{ - UnknownErrorID: "UnknownError", - WrongJsonErrorID: "WrongJsonError", + UnknownErrorID: "UnknownError", + WrongJSONErrorID: "WrongJsonError", + BadRequestErrorID: "BadRequestError", + ServiceUnavailableErrorID: "ServiceUnavailableError", - ApiKeyNotValidErrorID: "ApiKeyNotValidError", + APIKeyNotValidErrorID: "ApiKeyNotValidError", TooBigArrayAllocationErrorID: "TooBigArrayAllocationError", InvalidSignatureErrorID: "InvalidSignatureError", InvalidAddressErrorID: "InvalidAddressError", diff --git a/pkg/api/errors_test.go b/pkg/api/errors_test.go index a17f274d72..10f8ae9378 100644 --- a/pkg/api/errors_test.go +++ b/pkg/api/errors_test.go @@ -21,7 +21,7 @@ func TestErrorHandler_Handle(t *testing.T) { require.NoError(t, err) return string(data) } - badReqErr = &BadRequestError{errors.New("bad-request")} + badReqErr = apiErrs.NewBadRequestError(errors.New("bad-request")) unknownErr = apiErrs.NewUnknownError(errors.New("unknown")) defaultErr = errors.New("default") ) diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index d320818ead..e165394634 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -395,7 +395,7 @@ func (a *NodeApi) BlockScoreAt(w http.ResponseWriter, r *http.Request) error { id, err := strconv.ParseUint(s, 10, 64) if err != nil { // TODO(nickeskov): which error it should send? - return wrapToBadRequestError(err) + return apiErrs.NewBadRequestError(err) } rs, err := a.app.BlocksScoreAt(id) if err != nil { @@ -838,7 +838,7 @@ func (a *NodeApi) stateHash(w http.ResponseWriter, r *http.Request) error { height, err := strconv.ParseUint(s, 10, 64) if err != nil { // TODO(nickeskov): which error it should send? - return wrapToBadRequestError(err) + return apiErrs.NewBadRequestError(err) } if height < 1 { return apiErrs.BlockDoesNotExist @@ -884,7 +884,7 @@ func (a *NodeApi) snapshotStateHash(w http.ResponseWriter, r *http.Request) erro height, err := strconv.ParseUint(s, 10, 64) if err != nil { // TODO(nickeskov): which error it should send? - return wrapToBadRequestError(err) + return apiErrs.NewBadRequestError(err) } if height < 1 { return apiErrs.BlockDoesNotExist @@ -917,7 +917,13 @@ func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { if err != nil { return errors.Wrap(err, "invalid height") } - + isActivated, actErr := a.state.IsActivated(int16(settings.DeterministicFinality)) + if actErr != nil { + return errors.Wrap(actErr, "failed to check DeterministicFinality activation") + } + if !isActivated { + return apiErrs.NewUnavailableError(errors.New("deterministic finality is not activated")) + } activationHeight, err := a.state.ActivationHeight(int16(settings.DeterministicFinality)) if err != nil { return fmt.Errorf("failed to get DeterministicFinality activation height: %w", err) @@ -926,7 +932,7 @@ func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { periodStart, err := state.CurrentGenerationPeriodStart(activationHeight, height, a.app.settings.GenerationPeriod) if err != nil { - return err + return fmt.Errorf("failed to calculate generationPeriodStart: %w", err) } var generatorsInfo []GeneratorInfo @@ -939,7 +945,8 @@ func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { endorserRecipient := proto.NewRecipientFromAddress(generatorAddress) balance, pullErr := a.state.GeneratingBalance(endorserRecipient, height) if pullErr != nil { - return pullErr + return fmt.Errorf("failed to get generating balance for address %s at height %d: %w", + endorserRecipient.String(), height, pullErr) } generatorsInfo = append(generatorsInfo, GeneratorInfo{ Address: generatorAddress.String(), @@ -976,12 +983,18 @@ type SignCommitRequest struct { func (a *NodeApi) TransactionsSignCommit(w http.ResponseWriter, r *http.Request) error { var req SignCommitRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return errors.Wrap(err, "failed to decode JSON") + return apiErrs.NewBadRequestError(errors.Wrap(err, "failed to decode JSON")) + } + isActivated, activationErr := a.state.IsActivated(int16(settings.DeterministicFinality)) + if activationErr != nil { + return errors.Wrap(activationErr, "failed to check DeterministicFinality activation") + } + if !isActivated { + return apiErrs.NewUnavailableError(errors.New("deterministic finality is not activated")) } - addr, err := proto.NewAddressFromString(req.Sender) if err != nil { - return errors.Wrap(err, "invalid sender address") + return apiErrs.NewBadRequestError(errors.Wrap(err, "invalid sender address")) } now := time.Now().UnixMilli() @@ -992,9 +1005,8 @@ func (a *NodeApi) TransactionsSignCommit(w http.ResponseWriter, r *http.Request) } timestampUint, err := safecast.Convert[uint64](timestamp) if err != nil { - return errors.Wrap(err, "invalid timestamp") + return apiErrs.NewBadRequestError(errors.Wrap(err, "invalid timestamp")) } - var periodStart uint32 if req.GenerationPeriodStart != nil { periodStart = *req.GenerationPeriodStart diff --git a/pkg/api/peers.go b/pkg/api/peers.go index 2623b97a48..ac7f47ac5d 100644 --- a/pkg/api/peers.go +++ b/pkg/api/peers.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" + apiErrs "github.com/wavesplatform/gowaves/pkg/api/errors" "github.com/wavesplatform/gowaves/pkg/p2p/peer" "github.com/wavesplatform/gowaves/pkg/proto" "github.com/wavesplatform/gowaves/pkg/util/common" @@ -81,12 +82,12 @@ func (a *App) PeersConnect(ctx context.Context, apiKey string, addr string) (*Pe d := proto.NewTCPAddrFromString(addr) if d.Empty() { slog.Error("Invalid peer's address to connect", "address", addr) - return nil, wrapToBadRequestError(errors.New("invalid address")) + return nil, apiErrs.NewBadRequestError(errors.New("invalid address")) } err = a.peers.Connect(ctx, d) if err != nil { - return nil, wrapToBadRequestError(err) + return nil, apiErrs.NewBadRequestError(err) } return &PeersConnectResponse{ diff --git a/pkg/client/generators.go b/pkg/client/generators.go index 06a82668ee..15bbd51aec 100644 --- a/pkg/client/generators.go +++ b/pkg/client/generators.go @@ -5,7 +5,7 @@ import ( "fmt" "net/http" - "github.com/wavesplatform/gowaves/pkg/api" + nodeApi "github.com/wavesplatform/gowaves/pkg/api" ) // Generators is a client wrapper for generator-related API endpoints. @@ -27,7 +27,7 @@ type GeneratorsAtResponse struct { // CommitmentGeneratorsAt returns the list of committed generators for the given height. func (a *Generators) CommitmentGeneratorsAt(ctx context.Context, - height uint64) ([]api.GeneratorInfo, *Response, error) { + height uint64) ([]nodeApi.GeneratorInfo, *Response, error) { url, err := joinUrl(a.options.BaseUrl, fmt.Sprintf("/generators/at/%d", height)) if err != nil { return nil, nil, err @@ -36,7 +36,7 @@ func (a *Generators) CommitmentGeneratorsAt(ctx context.Context, if err != nil { return nil, nil, err } - var out []api.GeneratorInfo + var out []nodeApi.GeneratorInfo resp, err := doHTTP(ctx, a.options, req, &out) if err != nil { return nil, resp, err diff --git a/pkg/node/blocks_applier/node_mocks.go b/pkg/node/blocks_applier/node_mocks.go index 22d1c13812..e8e02674db 100644 --- a/pkg/node/blocks_applier/node_mocks.go +++ b/pkg/node/blocks_applier/node_mocks.go @@ -106,7 +106,8 @@ func (a *MockStateManager) Close() error { } func (a *MockStateManager) AddDeserializedBlocks( - blocks []*proto.Block) (*proto.Block, error) { + blocks []*proto.Block, +) (*proto.Block, error) { var out *proto.Block var err error for _, b := range blocks { diff --git a/pkg/state/threadsafe_wrapper.go b/pkg/state/threadsafe_wrapper.go index d5abf42135..87bca1c8ab 100644 --- a/pkg/state/threadsafe_wrapper.go +++ b/pkg/state/threadsafe_wrapper.go @@ -522,7 +522,8 @@ func (a *ThreadSafeWriteWrapper) AddBlocksWithSnapshots(blocks [][]byte, snapsho } func (a *ThreadSafeWriteWrapper) AddDeserializedBlocks( - blocks []*proto.Block) (*proto.Block, error) { + blocks []*proto.Block, +) (*proto.Block, error) { a.lock() defer a.unlock() return a.s.AddDeserializedBlocks(blocks) diff --git a/pkg/wallet/stub.go b/pkg/wallet/stub.go deleted file mode 100644 index 72e0b00197..0000000000 --- a/pkg/wallet/stub.go +++ /dev/null @@ -1,31 +0,0 @@ -package wallet - -import ( - "github.com/wavesplatform/gowaves/pkg/crypto" - "github.com/wavesplatform/gowaves/pkg/crypto/bls" - "github.com/wavesplatform/gowaves/pkg/proto" -) - -type Stub struct { - S [][]byte -} - -func (s Stub) SignTransactionWith(_ crypto.PublicKey, _ proto.Transaction) error { - panic("Stub.SignTransactionWith: Unsupported operation") -} - -func (s Stub) FindPublicKeyByAddress(_ proto.WavesAddress, _ proto.Scheme) (crypto.PublicKey, error) { - panic("Stub.FindPublicKeyByAddress: Unsupported operation") -} - -func (s Stub) BLSPairByWavesPK(_ crypto.PublicKey) (bls.SecretKey, bls.PublicKey, error) { - panic("Stub.BLSPairByWavesPK: Unsupported operation") -} - -func (s Stub) Load(_ []byte) error { - panic("Stub.Load: Unsupported operation") -} - -func (s Stub) AccountSeeds() [][]byte { - return s.S -} From 8b80e20fb2068158fd61b6de2acacee6089f3b28 Mon Sep 17 00:00:00 2001 From: esuwu Date: Tue, 23 Dec 2025 00:27:19 +0100 Subject: [PATCH 16/17] Implemented some AI suggestions --- pkg/api/node_api.go | 7 +++---- pkg/client/generators.go | 6 ------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index e165394634..895fc4a74b 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -934,13 +934,12 @@ func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { if err != nil { return fmt.Errorf("failed to calculate generationPeriodStart: %w", err) } - - var generatorsInfo []GeneratorInfo - generatorAddresses, err := a.state.CommittedGenerators(periodStart) if err != nil { return err } + + generatorsInfo := make([]GeneratorInfo, 0, len(generatorAddresses)) for _, generatorAddress := range generatorAddresses { endorserRecipient := proto.NewRecipientFromAddress(generatorAddress) balance, pullErr := a.state.GeneratingBalance(endorserRecipient, height) @@ -951,7 +950,7 @@ func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { generatorsInfo = append(generatorsInfo, GeneratorInfo{ Address: generatorAddress.String(), Balance: balance, - TransactionID: "", // TODO should be somehow found. + TransactionID: "", // It was decided to leave it empty. }) } return trySendJSON(w, generatorsInfo) diff --git a/pkg/client/generators.go b/pkg/client/generators.go index 15bbd51aec..af82db4a44 100644 --- a/pkg/client/generators.go +++ b/pkg/client/generators.go @@ -19,12 +19,6 @@ func NewGenerators(options Options) *Generators { } } -// GeneratorsAtResponse is the expected structure returned by /generators/at/{height}. -type GeneratorsAtResponse struct { - Height uint64 `json:"height"` - Generators []string `json:"generators"` -} - // CommitmentGeneratorsAt returns the list of committed generators for the given height. func (a *Generators) CommitmentGeneratorsAt(ctx context.Context, height uint64) ([]nodeApi.GeneratorInfo, *Response, error) { From 57f2cf68ddcdc8b4d8654ed0a57983bb6a8b2d87 Mon Sep 17 00:00:00 2001 From: esuwu Date: Tue, 23 Dec 2025 19:27:20 +0100 Subject: [PATCH 17/17] Made the tx signing handle all tx types --- itests/clients/http_client.go | 3 +- pkg/api/errors.go | 3 +- pkg/api/errors/basics.go | 36 ++++++++++++++ pkg/api/errors/consts.go | 8 +-- pkg/api/errors_test.go | 4 +- pkg/api/node_api.go | 91 ++++++++++++++++++++++++++--------- pkg/api/routes.go | 2 +- pkg/client/generators.go | 11 +++-- pkg/client/transactions.go | 1 + 9 files changed, 121 insertions(+), 38 deletions(-) diff --git a/itests/clients/http_client.go b/itests/clients/http_client.go index dc85d50034..8f592b04f8 100644 --- a/itests/clients/http_client.go +++ b/itests/clients/http_client.go @@ -10,7 +10,6 @@ import ( "github.com/wavesplatform/gowaves/itests/config" d "github.com/wavesplatform/gowaves/itests/docker" - nodeApi "github.com/wavesplatform/gowaves/pkg/api" "github.com/wavesplatform/gowaves/pkg/client" "github.com/wavesplatform/gowaves/pkg/crypto" "github.com/wavesplatform/gowaves/pkg/proto" @@ -176,7 +175,7 @@ func (c *HTTPClient) BlockFinalized(t testing.TB) *proto.BlockHeader { return header } -func (c *HTTPClient) CommitmentGeneratorsAt(t testing.TB, height proto.Height) []nodeApi.GeneratorInfo { +func (c *HTTPClient) CommitmentGeneratorsAt(t testing.TB, height proto.Height) []client.GeneratorInfoResponse { ctx, cancel := context.WithTimeout(context.Background(), c.timeout) defer cancel() diff --git a/pkg/api/errors.go b/pkg/api/errors.go index c62a1bcfd0..1e16746038 100644 --- a/pkg/api/errors.go +++ b/pkg/api/errors.go @@ -59,8 +59,7 @@ func (eh *ErrorHandler) Handle(w http.ResponseWriter, r *http.Request, err error ) switch { case errors.As(err, &badRequestError): - // nickeskov: this error type will be removed in future - http.Error(w, fmt.Sprintf("Failed to complete request: %s", badRequestError.Error()), http.StatusBadRequest) + eh.sendApiErrJSON(w, r, badRequestError) case errors.As(err, &authError): // nickeskov: this error type will be removed in future http.Error(w, fmt.Sprintf("Failed to complete request: %s", authError.Error()), http.StatusForbidden) diff --git a/pkg/api/errors/basics.go b/pkg/api/errors/basics.go index 3bbd27c12c..f0701bea5d 100644 --- a/pkg/api/errors/basics.go +++ b/pkg/api/errors/basics.go @@ -194,3 +194,39 @@ func NewBadRequestErrorWithMsg(message string, inner error) *BadRequestError { inner: inner, } } + +// NotImplementedError is a wrapper for any not implemented error. +type NotImplementedError struct { + genericError + inner error +} + +func (u *NotImplementedError) Unwrap() error { + return u.inner +} + +func (u *NotImplementedError) Error() string { + if u.Unwrap() != nil { + return fmt.Sprintf( + "%s; inner error (%T): %s", + u.genericError.Error(), + u.Unwrap(), u.Unwrap().Error(), + ) + } + return u.genericError.Error() +} + +func NewNotImplementedError(inner error) *NotImplementedError { + return NewNotImplementedErrorWithMsg("Not implemented", inner) +} + +func NewNotImplementedErrorWithMsg(message string, inner error) *NotImplementedError { + return &NotImplementedError{ + genericError: genericError{ + ID: NotImplementedErrorID, + HttpCode: http.StatusNotImplemented, + Message: message, + }, + inner: inner, + } +} diff --git a/pkg/api/errors/consts.go b/pkg/api/errors/consts.go index 99c942257a..57a80461a8 100644 --- a/pkg/api/errors/consts.go +++ b/pkg/api/errors/consts.go @@ -3,13 +3,14 @@ package errors const ( UnknownErrorID ErrorID = 0 WrongJSONErrorID ErrorID = 1 - BadRequestErrorID ErrorID = 2 - ServiceUnavailableErrorID ErrorID = 3 + BadRequestErrorID ErrorID = 3 + ServiceUnavailableErrorID ErrorID = 4 + NotImplementedErrorID ErrorID = 5 ) // API Auth const ( - APIKeyNotValidErrorID ApiAuthErrorID = 4 + APIKeyNotValidErrorID ApiAuthErrorID = 2 TooBigArrayAllocationErrorID ApiAuthErrorID = 10 ) @@ -62,6 +63,7 @@ var errorNames = map[Identifier]string{ WrongJSONErrorID: "WrongJsonError", BadRequestErrorID: "BadRequestError", ServiceUnavailableErrorID: "ServiceUnavailableError", + NotImplementedErrorID: "NotImplementedError", APIKeyNotValidErrorID: "ApiKeyNotValidError", TooBigArrayAllocationErrorID: "TooBigArrayAllocationError", diff --git a/pkg/api/errors_test.go b/pkg/api/errors_test.go index 10f8ae9378..d5b434dfff 100644 --- a/pkg/api/errors_test.go +++ b/pkg/api/errors_test.go @@ -35,13 +35,13 @@ func TestErrorHandler_Handle(t *testing.T) { name: "BadRequestErrorCase", err: errors.WithStack(errors.WithStack(badReqErr)), expectedCode: http.StatusBadRequest, - expectedBody: "Failed to complete request: bad-request\n", + expectedBody: mustJSON(badReqErr) + "\n", }, { name: "ErrorWithMultipleWraps", err: errors.Wrap(errors.Wrap(badReqErr, "wrap1"), "wrap2"), expectedCode: http.StatusBadRequest, - expectedBody: "Failed to complete request: bad-request\n", + expectedBody: mustJSON(badReqErr) + "\n", }, { name: "AuthErrorCase", diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index 895fc4a74b..24024708bb 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -905,7 +905,7 @@ func (a *NodeApi) snapshotStateHash(w http.ResponseWriter, r *http.Request) erro return nil } -type GeneratorInfo struct { +type generatorInfo struct { Address string `json:"address"` Balance uint64 `json:"balance"` TransactionID string `json:"transactionID"` @@ -939,7 +939,7 @@ func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { return err } - generatorsInfo := make([]GeneratorInfo, 0, len(generatorAddresses)) + generatorsInfo := make([]generatorInfo, 0, len(generatorAddresses)) for _, generatorAddress := range generatorAddresses { endorserRecipient := proto.NewRecipientFromAddress(generatorAddress) balance, pullErr := a.state.GeneratingBalance(endorserRecipient, height) @@ -947,7 +947,7 @@ func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error { return fmt.Errorf("failed to get generating balance for address %s at height %d: %w", endorserRecipient.String(), height, pullErr) } - generatorsInfo = append(generatorsInfo, GeneratorInfo{ + generatorsInfo = append(generatorsInfo, generatorInfo{ Address: generatorAddress.String(), Balance: balance, TransactionID: "", // It was decided to leave it empty. @@ -972,30 +972,73 @@ func (a *NodeApi) FinalizedHeader(w http.ResponseWriter, _ *http.Request) error return trySendJSON(w, blockHeader) } -type SignCommitRequest struct { +type signTxEnvelope struct { + Type proto.TransactionType `json:"type"` + Version byte `json:"version,omitempty"` +} + +func (a *NodeApi) transactionSign(w http.ResponseWriter, r *http.Request) error { + var signTx signTxEnvelope + if err := json.NewDecoder(r.Body).Decode(&signTx); err != nil { + return apiErrs.NewBadRequestError(errors.Wrap(err, "failed to decode tx envelope")) + } + + tx, err := proto.GuessTransactionType(&proto.TransactionTypeVersion{ + Type: signTx.Type, + Version: signTx.Version, + }) + if err != nil { + return apiErrs.NewBadRequestError(err) + } + + switch tx.GetType() { + case proto.CommitToGenerationTransaction: + var req signCommit + if decodeErr := json.NewDecoder(r.Body).Decode(&req); decodeErr != nil { + return apiErrs.NewBadRequestError(errors.Wrap(decodeErr, "failed to decode JSON")) + } + signedTx, signErr := a.transactionsSignCommitToGeneration(req) + if signErr != nil { + return signErr + } + return trySendJSON(w, signedTx) + case proto.GenesisTransaction, proto.PaymentTransaction, proto.IssueTransaction, proto.TransferTransaction, + proto.ReissueTransaction, proto.BurnTransaction, proto.ExchangeTransaction, proto.LeaseTransaction, + proto.LeaseCancelTransaction, proto.CreateAliasTransaction, proto.MassTransferTransaction, proto.DataTransaction, + proto.SetScriptTransaction, proto.SponsorshipTransaction, proto.SetAssetScriptTransaction, + proto.InvokeScriptTransaction, proto.UpdateAssetInfoTransaction, proto.EthereumMetamaskTransaction, + proto.InvokeExpressionTransaction: + return apiErrs.NewNotImplementedError(errors.Errorf("transaction signing not implemented for type %d", tx.GetType())) + default: + return apiErrs.NewBadRequestError(errors.Errorf("unknown transaction type %d", tx.GetType())) + } +} + +type signCommit struct { Sender string `json:"sender"` GenerationPeriodStart *uint32 `json:"generationPeriodStart,omitempty"` Timestamp *int64 `json:"timestamp,omitempty"` ChainID *byte `json:"chainId,omitempty"` + Type byte `json:"type,omitempty"` + Version byte `json:"version,omitempty"` } -func (a *NodeApi) TransactionsSignCommit(w http.ResponseWriter, r *http.Request) error { - var req SignCommitRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return apiErrs.NewBadRequestError(errors.Wrap(err, "failed to decode JSON")) - } +func (a *NodeApi) transactionsSignCommitToGeneration(req signCommit) (*proto.CommitToGenerationWithProofs, error) { isActivated, activationErr := a.state.IsActivated(int16(settings.DeterministicFinality)) if activationErr != nil { - return errors.Wrap(activationErr, "failed to check DeterministicFinality activation") + return nil, errors.Wrap(activationErr, "failed to check DeterministicFinality activation") } if !isActivated { - return apiErrs.NewUnavailableError(errors.New("deterministic finality is not activated")) + return nil, apiErrs.NewUnavailableError(errors.New("deterministic finality is not activated")) } addr, err := proto.NewAddressFromString(req.Sender) if err != nil { - return apiErrs.NewBadRequestError(errors.Wrap(err, "invalid sender address")) + return nil, apiErrs.NewBadRequestError(errors.Wrap(err, "invalid sender address")) + } + scheme := a.app.services.Scheme + if req.ChainID != nil { + scheme = *req.ChainID } - now := time.Now().UnixMilli() timestamp := now @@ -1004,7 +1047,7 @@ func (a *NodeApi) TransactionsSignCommit(w http.ResponseWriter, r *http.Request) } timestampUint, err := safecast.Convert[uint64](timestamp) if err != nil { - return apiErrs.NewBadRequestError(errors.Wrap(err, "invalid timestamp")) + return nil, apiErrs.NewBadRequestError(errors.Wrap(err, "invalid timestamp")) } var periodStart uint32 if req.GenerationPeriodStart != nil { @@ -1012,32 +1055,32 @@ func (a *NodeApi) TransactionsSignCommit(w http.ResponseWriter, r *http.Request) } else { height, retrieveErr := a.state.Height() if retrieveErr != nil { - return errors.Wrap(retrieveErr, "failed to get height") + return nil, errors.Wrap(retrieveErr, "failed to get height") } activationH, actErr := a.state.ActivationHeight(int16(settings.DeterministicFinality)) if actErr != nil { - return errors.Wrap(actErr, "failed to get DF activation height") + return nil, errors.Wrap(actErr, "failed to get DF activation height") } periodStart, err = state.CurrentGenerationPeriodStart(activationH, height, a.app.settings.GenerationPeriod) if err != nil { - return errors.Wrap(err, "failed to calculate generationPeriodStart") + return nil, errors.Wrap(err, "failed to calculate generationPeriodStart") } } - senderPK, err := a.app.services.Wallet.FindPublicKeyByAddress(addr, a.app.services.Scheme) + senderPK, err := a.app.services.Wallet.FindPublicKeyByAddress(addr, scheme) if err != nil { - return errors.Wrap(err, "failed to find key pair by address") + return nil, errors.Wrap(err, "failed to find key pair by address") } blsSecretKey, blsPublicKey, err := a.app.services.Wallet.BLSPairByWavesPK(senderPK) if err != nil { - return errors.Wrap(err, "failed to find endorser public key by generator public key") + return nil, errors.Wrap(err, "failed to find endorser public key by generator public key") } _, commitmentSignature, popErr := bls.ProvePoP(blsSecretKey, blsPublicKey, periodStart) if popErr != nil { - return errors.Wrap(popErr, "failed to create proof of possession for commitment") + return nil, errors.Wrap(popErr, "failed to create proof of possession for commitment") } - tx := proto.NewUnsignedCommitToGenerationWithProofs(1, + tx := proto.NewUnsignedCommitToGenerationWithProofs(req.Version, senderPK, periodStart, blsPublicKey, @@ -1046,9 +1089,9 @@ func (a *NodeApi) TransactionsSignCommit(w http.ResponseWriter, r *http.Request) timestampUint) err = a.app.services.Wallet.SignTransactionWith(senderPK, tx) if err != nil { - return err + return nil, err } - return trySendJSON(w, tx) + return tx, nil } func wavesAddressInvalidCharErr(invalidChar rune, id string) *apiErrs.CustomValidationError { diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 72334e7f2e..452440fddb 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -146,7 +146,7 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { r.Post("/broadcast", wrapper(a.TransactionsBroadcast)) rAuth := r.With(checkAuthMiddleware) - rAuth.Post("/sign", wrapper(a.TransactionsSignCommit)) + rAuth.Post("/sign", wrapper(a.transactionSign)) }) r.Route("/peers", func(r chi.Router) { diff --git a/pkg/client/generators.go b/pkg/client/generators.go index af82db4a44..60b85555c8 100644 --- a/pkg/client/generators.go +++ b/pkg/client/generators.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "net/http" - - nodeApi "github.com/wavesplatform/gowaves/pkg/api" ) // Generators is a client wrapper for generator-related API endpoints. @@ -19,9 +17,14 @@ func NewGenerators(options Options) *Generators { } } +type GeneratorInfoResponse struct { + Address string `json:"address"` + Balance uint64 `json:"balance"` +} + // CommitmentGeneratorsAt returns the list of committed generators for the given height. func (a *Generators) CommitmentGeneratorsAt(ctx context.Context, - height uint64) ([]nodeApi.GeneratorInfo, *Response, error) { + height uint64) ([]GeneratorInfoResponse, *Response, error) { url, err := joinUrl(a.options.BaseUrl, fmt.Sprintf("/generators/at/%d", height)) if err != nil { return nil, nil, err @@ -30,7 +33,7 @@ func (a *Generators) CommitmentGeneratorsAt(ctx context.Context, if err != nil { return nil, nil, err } - var out []nodeApi.GeneratorInfo + var out []GeneratorInfoResponse resp, err := doHTTP(ctx, a.options, req, &out) if err != nil { return nil, resp, err diff --git a/pkg/client/transactions.go b/pkg/client/transactions.go index 37486050c9..041e1b908a 100644 --- a/pkg/client/transactions.go +++ b/pkg/client/transactions.go @@ -189,6 +189,7 @@ type SignCommitRequest struct { Sender string `json:"sender"` GenerationPeriodStart *uint32 `json:"generationPeriodStart,omitempty"` Timestamp *int64 `json:"timestamp,omitempty"` + ChainID *byte `json:"chainId,omitempty"` } // SignCommit calls POST /transactions/sign and returns the signed CommitToGenerationWithProofs tx.