Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
38b9a0d
WIP: Base itest suite added with support of feature 25 added.
alexeykiselev Apr 13, 2026
5898f71
Fixed Docker connection API version.
alexeykiselev Apr 15, 2026
9982d9f
Rise level of log message.
alexeykiselev Apr 15, 2026
4d4bd2d
Scala node commitment for generation added to finality smoke itest.
alexeykiselev Apr 16, 2026
9c31ae1
Correct BLS keys generation for Go and Scala miners.
alexeykiselev Apr 16, 2026
87e51e6
Fixed BLS key generation for non-mining accounts.
alexeykiselev Apr 16, 2026
16778ec
Copilot review issues fixed.
alexeykiselev Apr 16, 2026
7245f18
Review fixes.
alexeykiselev Apr 17, 2026
90b617b
WIP. Function to burn deposit added.
alexeykiselev Apr 21, 2026
8a23896
WIP. TransactionID field added to commitments item.
alexeykiselev Apr 21, 2026
4af54c0
New way of storing and initializing of generator set implemented.
alexeykiselev Apr 23, 2026
109c7d3
Merge branch 'determenistic-finality-feature' into burn-deposit
alexeykiselev Apr 23, 2026
6f125ff
Default values for new setting added.
alexeykiselev Apr 23, 2026
c830be1
Fixed height issue then querying generating balance from generator set.
alexeykiselev Apr 24, 2026
19153b2
Merge branch 'determenistic-finality-feature' into burn-deposit
alexeykiselev Apr 27, 2026
f3b0bca
NG activation check added to mined block action of Sync state.
alexeykiselev Apr 28, 2026
446a034
Fixed copilot review issues.
alexeykiselev Apr 28, 2026
8a51c43
Fixed JSON names for finalized block API.
alexeykiselev Apr 28, 2026
aacf0f8
Duplicate sync peer field removed.
alexeykiselev Apr 29, 2026
443cd7e
Endorsements validation during block application depend on finalized …
alexeykiselev Apr 29, 2026
c512489
Peers suspending replaced with blacklisting.
alexeykiselev Apr 30, 2026
cae8e1f
Switching to Idle state directly after receiving empty signatures fro…
alexeykiselev Apr 30, 2026
eed3b3b
Select peer with the maximus score if no peer sync selected.
alexeykiselev May 4, 2026
f6eb621
Merge branch 'determenistic-finality-feature' into burn-deposit
alexeykiselev May 6, 2026
5b022be
Merge branch 'determenistic-finality-feature' into burn-deposit
alexeykiselev May 14, 2026
3169f46
Review fixes.
alexeykiselev May 14, 2026
d025cfb
Merge branch 'determenistic-finality-feature' into burn-deposit
alexeykiselev May 21, 2026
a7ab7ea
GeneratorInfo balance is zeroed on ban of generator.
alexeykiselev May 21, 2026
bd48325
Correct deposit reset for conflicting endorsers implemented.
alexeykiselev May 22, 2026
6aa3c0a
Bug of resetting only one deposit fixed.
alexeykiselev May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions itests/config/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ func NewBlockchainConfig(options ...BlockchainOption) (*BlockchainConfig, error)
bs.BlockRewardTerm = defaultBlockRewardTerm
bs.MinXTNBuyBackPeriod = defaultMinXTNBuyBackPeriod
bs.LightNodeBlockFieldsAbsenceInterval = lightNodeBlockFieldsAbsenceInterval
bs.GenerationPeriod = defaultGenerationPeriod
bs.MaxEndorsements = defaultMaxEndorsements

cfg := &BlockchainConfig{
Settings: bs,
Expand Down
3 changes: 3 additions & 0 deletions itests/config/genesis_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const (
defaultQuorum = 1 // Default quorum is 1 to allow mining without waiting for testnet client.

lightNodeBlockFieldsAbsenceInterval = 2

defaultGenerationPeriod = 5
defaultMaxEndorsements = 3
)

var (
Expand Down
34 changes: 13 additions & 21 deletions pkg/api/node_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -906,13 +906,6 @@ func (a *NodeApi) snapshotStateHash(w http.ResponseWriter, r *http.Request) erro
return nil
}

// TODO: Move JSON tags to GeneratorInfo structure.
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)
Expand All @@ -935,32 +928,31 @@ func (a *NodeApi) GeneratorsAt(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return err
}

infos := make([]generatorInfo, len(gs))
for i, g := range gs {
infos[i] = generatorInfo{
Address: g.Address().String(),
Balance: g.GenerationBalance(),
TransactionID: "", // It was decided to leave it empty.
}
}
return trySendJSON(w, infos)
return trySendJSON(w, gs)
Comment on lines 919 to +923
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GeneratorsAt now returns []state.GeneratorInfo directly. This changes the public JSON shape compared to the previous response wrapper:

  • additional fields like PublicKey and Ban will now be exposed
  • JSON field names will follow Go identifiers for untagged fields (e.g. PublicKey, Ban), which is inconsistent with the endpoint’s existing lower-camel JSON convention.

If the intent is to expose only address, balance, and transactionID (or to keep stable field naming), consider keeping an explicit response DTO / adding JSON tags (or json:"-") on fields that should not be part of the API contract.

Copilot uses AI. Check for mistakes.
}

func (a *NodeApi) FinalizedHeight(w http.ResponseWriter, _ *http.Request) error {
h, err := a.state.LastFinalizedHeight()
if err != nil {
return err
return fmt.Errorf("failed to get finalized block height: %w", err)
}
return trySendJSON(w, map[string]uint64{"height": h})
}

func (a *NodeApi) FinalizedHeader(w http.ResponseWriter, _ *http.Request) error {
blockHeader, err := a.app.state.LastFinalizedBlock()
fh, err := a.state.LastFinalizedHeight()
if err != nil {
return err
return fmt.Errorf("failed to get finalized block height: %w", err)
}
header, err := a.app.state.LastFinalizedBlock()
if err != nil {
return fmt.Errorf("failed to get finalized block header: %w", err)
}
b, err := newAPIBlockFromHeader(*header, a.app.scheme(), fh)
if err != nil {
return fmt.Errorf("failed to get finalized block: %w", err)
}
return trySendJSON(w, blockHeader)
return trySendJSON(w, b)
}

type signTxEnvelope struct {
Expand Down
48 changes: 32 additions & 16 deletions pkg/node/fsm/ng_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,16 +258,29 @@ func (a *NGState) endorseParentWithEachKey(
block *proto.Block,
blockHeight proto.Height,
) error {
activationHeight, err := a.baseInfo.storage.ActivationHeight(int16(settings.DeterministicFinality))
if err != nil {
return a.Errorf(errors.Wrapf(err, "failed to get activation height for finality %s", block.BlockID()))
// Check current generators set.
gs, err := a.baseInfo.storage.CommittedGenerators(blockHeight)
if err != nil && !errors.Is(err, state.ErrNoGeneratorsSet) {
return a.Errorf(errors.Wrapf(err, "failed to get generators for block '%s'", block.BlockID()))
}

periodStart, err := state.CurrentGenerationPeriodStart(activationHeight, blockHeight, a.baseInfo.generationPeriod)
if err != nil {
return a.Errorf(errors.Wrapf(err, "failed to get current generation period, block %s", block.BlockID()))
if errors.Is(err, state.ErrNoGeneratorsSet) || len(gs) == 0 {
slog.Debug("Generator set is empty, skipping block endorsement", slog.Uint64("height", blockHeight))
return nil
}

// Following variables are used for logging only.
var activationHeight proto.Height
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable declaration can be moved inside if block.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

var periodStart uint32
if slog.Default().Enabled(context.Background(), slog.LevelDebug) {
activationHeight, err = a.baseInfo.storage.ActivationHeight(int16(settings.DeterministicFinality))
if err != nil {
return a.Errorf(errors.Wrapf(err, "failed to get activation height for finality %s", block.BlockID()))
}
periodStart, err = state.CurrentGenerationPeriodStart(activationHeight, blockHeight, a.baseInfo.generationPeriod)
if err != nil {
return a.Errorf(errors.Wrapf(err, "failed to get current generation period, block %s", block.BlockID()))
}
}
for i := range sks {
sk := sks[i]
pk, pkErr := sk.PublicKey()
Expand All @@ -278,13 +291,13 @@ func (a *NGState) endorseParentWithEachKey(
slog.Int("SeedIndex", i), slog.String("BLS PublicKey", pk.String()),
slog.Any("BlockID", block.BlockID()), slog.Any("GenerationPeriodStart", periodStart))

g, gErr := a.baseInfo.storage.FindGenerator(state.ByBLSPublicKey(pk))
g, gErr := a.baseInfo.storage.FindGenerator(blockHeight, state.ByBLSPublicKey(pk))
if gErr != nil {
slog.Warn("Wallet's BLS public key is not in the generators set",
slog.String("BLS PublicKey", pk.String()), logging.Error(gErr))
continue
}
if g.GenerationBalance() == 0 {
if g.Balance == 0 {
slog.Debug("Wallet's BLS public key has insufficient generation balance",
slog.Int("SeedIndex", i), slog.String("BLS PublicKey", pk.String()),
slog.Any("BlockID", block.BlockID()), slog.Any("GenerationPeriodStart", periodStart))
Expand Down Expand Up @@ -316,12 +329,15 @@ func (a *NGState) BlockEndorsement(blockEndorsement *proto.BlockEndorsement) (St
}

top := a.baseInfo.storage.TopBlock()

h, err := a.baseInfo.storage.Height()
if err != nil {
return a, nil, a.Errorf(errors.Wrapf(err, "failed to retrieve current height"))
}
generatorIndex, err := safecast.Convert[uint32](blockEndorsement.EndorserIndex)
if err != nil {
return a, nil, a.Errorf(errors.Wrapf(err, "failed to convert endorser index to uint32"))
}
gi, err := a.baseInfo.storage.FindGenerator(state.ByIndex(generatorIndex))
gi, err := a.baseInfo.storage.FindGenerator(h, state.ByIndex(generatorIndex))
if err != nil {
return a, nil, a.Errorf(errors.Wrapf(err, "failed to find generator by index"))
}
Expand All @@ -334,8 +350,8 @@ func (a *NGState) BlockEndorsement(blockEndorsement *proto.BlockEndorsement) (St
return a, nil, a.Errorf(errors.Wrapf(err, "failed to get last finalized block header for endorser address"))
}
// TODO check if generator is in the generator set.
endorserPK := gi.BLSPublicKey()
balance := gi.GenerationBalance()
endorserPK := gi.BLSPublicKey
balance := gi.Balance
added, addErr := a.baseInfo.endorsements.Add(blockEndorsement, endorserPK,
localFinalizedHeight, localFinalizedBlockHeader.BlockID(), balance, top.Parent)
if addErr != nil {
Expand Down Expand Up @@ -457,7 +473,7 @@ func (a *NGState) MinedBlock(

func (a *NGState) Endorse(parentBlockID proto.BlockID,
endorser state.GeneratorInfo, endorserSK bls.SecretKey) error {
endorserIndex := endorser.Index()
endorserIndex := endorser.Index
lastFinalizedHeight, err := a.baseInfo.storage.LastFinalizedHeight()
if err != nil {
return a.Errorf(errors.Wrap(err, "failed to get last finalized block height"))
Expand Down Expand Up @@ -514,10 +530,10 @@ func (a *NGState) addAndBroadcastOwnEndorsement(
top := a.baseInfo.storage.TopBlock()
added, addErr := a.baseInfo.endorsements.Add(
parentBlockEndorsement,
endorser.BLSPublicKey(),
endorser.BLSPublicKey,
lastFinalizedHeight,
lastFinalizedBlockID,
endorser.GenerationBalance(),
endorser.Balance,
top.Parent,
)
if addErr != nil {
Expand Down
25 changes: 15 additions & 10 deletions pkg/node/fsm/sync_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/wavesplatform/gowaves/pkg/p2p/peer"
"github.com/wavesplatform/gowaves/pkg/p2p/peer/extension"
"github.com/wavesplatform/gowaves/pkg/proto"
"github.com/wavesplatform/gowaves/pkg/settings"
"github.com/wavesplatform/gowaves/pkg/state"
"github.com/wavesplatform/gowaves/pkg/types"
)
Expand Down Expand Up @@ -200,22 +201,26 @@ func (a *SyncState) BlockSnapshot(
func (a *SyncState) MinedBlock(
block *proto.Block, limits proto.MiningLimits, keyPair proto.KeyPair, vrf []byte,
) (State, Async, error) {
height, heightErr := a.baseInfo.storage.Height()
if heightErr != nil {
return a, nil, a.Errorf(heightErr)
height, err := a.baseInfo.storage.Height()
if err != nil {
return a, nil, a.Errorf(err)
}
ngActivated, err := a.baseInfo.storage.IsActiveAtHeight(int16(settings.NG), height)
if err != nil {
return a, nil, a.Errorf(err)
}
if ngActivated {
slog.Debug("Skipping mined block in Sync state because NG is already active", "state", a.String())
return a, nil, nil
Comment on lines +214 to +224
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MinedBlock() checks NG activation at the current state height (IsActiveAtHeight(..., height)), but the mined block would be applied at height+1. Other mining paths check feature activation for the next height (e.g. NGState.mineMicro() uses height+1).

This can mis-handle the activation boundary (mining/applying a block in Sync state when NG activates at the next height). Consider checking IsActiveAtHeight(..., height+1) here for consistency with the rest of the codebase.

Copilot uses AI. Check for mistakes.
}
metrics.BlockMined(block)
a.baseInfo.logger.Info("New block mined", "state", a.String(), "blockID", block.ID.String())

_, err := a.baseInfo.blocksApplier.Apply(
a.baseInfo.storage,
[]*proto.Block{block},
)
if err != nil {
slog.Warn("Failed to apply mined block", slog.String("state", a.String()), logging.Error(err))
if _, apErr := a.baseInfo.blocksApplier.Apply(a.baseInfo.storage, []*proto.Block{block}); apErr != nil {
slog.Warn("Failed to apply mined block", slog.String("state", a.String()), logging.Error(apErr))
return a, nil, nil // We've failed to apply mined block, it's not an error
}
metrics.BlockAppliedFromExtension(block, height+1)
metrics.BlockApplied(block, height+1)
a.baseInfo.scheduler.Reschedule()

// first we should send block
Expand Down
23 changes: 18 additions & 5 deletions pkg/proto/finalization.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ func (msg *EndorsementCryptoMessage) Bytes() ([]byte, error) {
// BlockEndorsement represents an endorsement of a block by a validator.
type BlockEndorsement struct {
EndorserIndex uint32 `json:"endorserIndex"`
FinalizedBlockID BlockID `json:"finalizedBlockID"`
FinalizedBlockHeight uint32 `json:"finalizedBlockHeight"`
FinalizedBlockID BlockID `json:"finalizedBlockId"`
FinalizedBlockHeight uint32 `json:"finalizedHeight"`
Comment on lines 60 to +62
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON tags for BlockEndorsement were renamed (finalizedBlockID -> finalizedBlockId, finalizedBlockHeight -> finalizedHeight). This changes the JSON wire format for any API/client that serializes these structs.

If backward compatibility matters, consider a transition plan (dual fields/custom marshaling) or ensure all JSON consumers are updated in the same release.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field names were brought into line with Scala implementation.

EndorsedBlockID BlockID `json:"endorsedBlockId"`
Signature bls.Signature `json:"signature"`
}
Expand Down Expand Up @@ -110,10 +110,10 @@ func (e *BlockEndorsement) ToProtobuf() (*g.EndorseBlock, error) {
}

type FinalizationVoting struct {
EndorserIndexes []uint32 `json:"endorserIndexes"`
FinalizedBlockHeight Height `json:"finalizedBlockHeight"`
EndorserIndexes []uint32 `json:"endorserIndexes,omitempty"`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nil slice -> null, empty non nil slice -> []. Is it intended behavior for EndorserIndexes and ConflictEndorsements?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in Scala implementation the field completely omitted if empty.

FinalizedBlockHeight Height `json:"finalizedHeight"`
AggregatedEndorsementSignature bls.Signature `json:"aggregatedEndorsementSignature"`
ConflictEndorsements []BlockEndorsement `json:"conflictEndorsements"`
ConflictEndorsements []BlockEndorsement `json:"conflictEndorsements,omitempty"`
Comment on lines 112 to +116
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FinalizationVoting JSON tags were renamed and omitempty was added (e.g. finalizedBlockHeight -> finalizedHeight, and endorserIndexes / conflictEndorsements now omit when empty). This is a JSON wire-format change.

If external consumers rely on the previous field names/presence, consider keeping compatibility or versioning the API.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok, no release yet.

}

// Validate checks that FinalizationVoting doesn't have any duplicate endorsers indexes.
Expand All @@ -137,6 +137,19 @@ func (f *FinalizationVoting) Validate() error {
return nil
}

// CheckSizes validates the sizes of finalization fields against the size of generator set.
// The number of endorsements and conflicting endorsements must not exceed the generator set size.
func (f *FinalizationVoting) CheckSizes(generatorSetSize int) error {
if ces := len(f.ConflictEndorsements); ces > generatorSetSize {
return fmt.Errorf("conflicting endorsements count %d exceeds generator set size %d",
ces, generatorSetSize)
}
if eis := len(f.EndorserIndexes); eis > generatorSetSize {
return fmt.Errorf("endorsements count %d exceeds generator set size %d", eis, generatorSetSize)
}
return nil
}

func (f *FinalizationVoting) Marshal() ([]byte, error) {
endBlockProto, err := f.ToProtobuf()
if err != nil {
Expand Down
Loading
Loading