Skip to content

Burn deposit#2070

Open
alexeykiselev wants to merge 26 commits into
determenistic-finality-featurefrom
burn-deposit
Open

Burn deposit#2070
alexeykiselev wants to merge 26 commits into
determenistic-finality-featurefrom
burn-deposit

Conversation

@alexeykiselev
Copy link
Copy Markdown
Collaborator

No description provided.

Simple two node finality test added.
Ordering of commitments state hash records fixed with new key.
Updated genesis.json configuration to support go/scala/no -mining.
Enabled log level check added before querying latest finalized block height.
Function to retrieve and compare finalized blocks heights added and called in test.
Section about env vars added to itest README.
New way of storing block generators.
Storing of transaction ID added to commitments.
JSON tags added to GeneratorInfo which in turn used in API.
@alexeykiselev alexeykiselev added bug Something isn't working wip This is a WIP, should not be merged right away labels Apr 23, 2026
Functions complexities updated for RideV9.
Test on RideV9 estimation added.
Logging improved.
@alexeykiselev alexeykiselev removed the wip This is a WIP, should not be merged right away label Apr 24, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR expands deterministic finality / generator-set handling by persisting generator sets per height, adding transaction IDs to generator commitments, and introducing deposit burning for punished generators, alongside related API/mock/test updates and Ride V9 catalogue adjustments.

Changes:

  • Extend commitments to store TransactionID and propagate the new store(..., txID, blockID) signature across state logic and tests.
  • Replace in-memory generator-set handling with a per-height persisted generatorsStorage, add deposit burning on punishment, and update finalization/generator lookup APIs accordingly.
  • Update Ride V9 function cost catalogue and add a Scala-compatibility estimation test; adjust itest blockchain defaults for finality-related settings.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pkg/state/transaction_checker_test.go Updates tests for new commitments store() signature (adds tx ID).
pkg/state/threadsafe_wrapper.go Updates thread-safe API wrapper for FindGenerator(height, lookup).
pkg/state/state_hasher_test.go Adjusts tests for commitments storing tx IDs.
pkg/state/state.go Renames generators storage type and threads height into FindGenerator; adjusts generator retrieval method name and finalization logging level.
pkg/state/snapshot_applier.go Stores commitment tx ID when applying commit-to-generation snapshots.
pkg/state/mock_state.go Regenerates mocks to match the new FindGenerator(height, lookup) API.
pkg/state/keys.go Renames key prefix/entity mapping from banned-generators to generators storage.
pkg/state/history_storage.go Renames blockchain entity from bannedGenerators to generators.
pkg/state/generators.go Major refactor: persist generator sets per height; implement punish/burn-deposit logic; expose generator lookups by height.
pkg/state/finalizer.go Updates finalization checks and generator access to use per-height generators storage and new voting size helper.
pkg/state/commitments_internal_test.go Updates commitments tests for new tx ID field and adds helper for random digests.
pkg/state/commitments.go Adds TransactionID to stored commitments and updates store() signature.
pkg/state/balances.go Adds burnDeposit() balance mutation used by generator punishment.
pkg/state/api.go Updates StateInfo interface for FindGenerator(height, lookup).
pkg/ride/tree_estimation_test.go Adds Ride V9 Scala compatibility estimation test.
pkg/ride/generate/internal/functions_generation.go Updates Ride V9 catalogue entries/costs.
pkg/ride/functions.gen.go Regenerates Ride V9 function map and catalogues.
pkg/proto/finalization.go Adds FinalizationVoting.CheckSizes() helper used by finalizer.
pkg/node/fsm/ng_state.go Updates generator lookups/endorsement flow for the height-aware generator APIs.
pkg/api/node_api.go Simplifies /generators/{height} to return []state.GeneratorInfo directly.
itests/config/genesis_settings.go Introduces defaults for generation period and max endorsements in itests.
itests/config/blockchain.go Applies new itest defaults to blockchain settings.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 692 to +719
tx, ok := a.txSnapshotContext.applyingTx.(*proto.CommitToGenerationWithProofs)
if !ok {
return errors.New("failed to apply generation commitment snapshot: applying tx is not CommitToGenerationWithProofs")
}
// We need to update deposit info for the transaction.
addr, err := proto.NewAddressFromPublicKey(a.info.Scheme(), snapshot.SenderPublicKey)
if err != nil {
return errors.Wrapf(err, "failed to create address from scheme %d and PK %q",
a.info.Scheme(), snapshot.SenderPublicKey.String())
}
profile, err := a.stor.balances.newestWavesBalance(addr.ID())
if err != nil {
return errors.Wrapf(err, "failed to get newest waves balance profile for address %q", addr.String())
}
if profile.Deposit >= 2*Deposit {
return errors.Errorf("invalid deposit in profile for address %q: %d", addr.String(), profile.Deposit)
}
newProfile := profile
newProfile.Deposit, err = common.AddInt(profile.Deposit, Deposit)
if err != nil {
return errors.Wrapf(err, "failed to add deposit to profile for address %q", addr.String())
}
value := newWavesValue(profile, newProfile)
if err = a.stor.balances.setWavesBalance(addr.ID(), value, a.info.BlockID()); err != nil {
return errors.Wrapf(err, "failed to get set balance profile for address %q", addr.String())
}
return a.stor.commitments.store(tx.GenerationPeriodStart, snapshot.SenderPublicKey, snapshot.EndorserPublicKey,
a.info.BlockID())
*tx.ID, a.info.BlockID())
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

ApplyCommitToGeneration() dereferences *tx.ID without ensuring the ID has been generated. CommitToGenerationWithProofs.ID can be nil until GetID()/GenerateID() is called, so this can panic when applying snapshots. Use tx.GetID(...) and convert the returned bytes to crypto.Digest, or otherwise ensure tx.ID is set before dereferencing.

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.

Fixed by calling GenerateID before accessing ID field. GenerateID() is no-op if a transaction already has ID.

Comment thread pkg/state/balances.go
Comment on lines +1017 to +1035
// burnDeposit reduces the balance of an account by Deposit amount. The Deposit part of balance profile is not affected.
func (s *balances) burnDeposit(addr proto.AddressID, blockID proto.BlockID) error {
balance, err := s.newestWavesBalance(addr)
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}
balance.Balance, err = common.SubInt(balance.Balance, Deposit)
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}
v := wavesValue{
profile: balance,
leaseChange: false,
balanceChange: true,
}
if sbErr := s.setWavesBalance(addr, v, blockID); sbErr != nil {
return fmt.Errorf("failed to burn deposit: %w", sbErr)
}
return nil
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

burnDeposit() introduces new balance mutation logic but there are no unit tests covering it. Since balances.go already has tests, please add coverage for: (1) successful burn reduces Balance by Deposit while leaving Deposit field unchanged, and (2) insufficient funds returns an error (no underflow).

Copilot uses AI. Check for mistakes.
Comment thread pkg/state/finalizer.go Outdated
finalizedHeight := height - 1
slog.Info("Block finalization achieved", slog.Uint64("finalizedHeight", finalizedHeight),
slog.Debug("Block finalization achieved", slog.Uint64("finalizedHeight", finalizedHeight),
slog.Uint64("blockHeigh", height), slog.String("blockID", blockID.String()))
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

Log field key blockHeigh looks like a typo; should likely be blockHeight for consistency with other structured logs.

Suggested change
slog.Uint64("blockHeigh", height), slog.String("blockID", blockID.String()))
slog.Uint64("blockHeight", height), slog.String("blockID", blockID.String()))

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.

Fixed.

Comment thread pkg/state/commitments.go
Comment on lines +108 to 111
rec.append(generatorPK, endorserPK, txID)
newData, mErr := rec.marshalBinary()
if mErr != nil {
return fmt.Errorf("failed to marshal commitments record: %w", mErr)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

Commitments now persist TransactionID in commitmentItem, but the state-hash component produced during store() still hashes only the generator and endorser public keys (see commitmentsRecordForStateHashes). If state hashes are expected to cover the persisted commitment record contents, the hashing logic should incorporate txID too; otherwise changing TransactionID would not affect the commitments-related state hash.

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.

No, transaction ID is not included in the legacy state hash. We store transactions IDs for information purposes of API only.

Comment thread pkg/state/generators.go
Comment on lines +333 to +341
default:
// Here we are crossing the edge of generation period, new commitments and previous generator set with a
// different generation period start. First of all, we should punish conflicting endorsements for the last
// block of previous generation period, and then create a new generator set.
if pErr := g.punish(pg, blockHeight, blockID); pErr != nil {
return fmt.Errorf("failed to punish conflicting endorsements of height %d: %w", blockchainHeight, pErr)
}
return g.initializeNewGeneratorsSetFromCommitments(cms, periodStart, threshold, genPK, blockHeight,
blockchainHeight, blockID)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

In the generation-period transition branch, punish() is called with blockHeight (current height) instead of blockchainHeight (last applied height). Since punish() burns deposits for generators banned on the previous block (BanHeight == blockchainHeight), passing blockHeight will skip burning deposits for bans from the immediately preceding block when crossing the period boundary.

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.

Yes, but there is special case in the same switch for such cases.

Comment thread pkg/state/generators.go
Comment on lines +654 to +661
func (g *generatorsStorage) totalGenerationBalance(height proto.Height) (uint64, error) {
gs, err := g.generators(height)
if err != nil {
if !errors.Is(err, ErrNoGeneratorsSet) {
return 0, nil
}
return 0, fmt.Errorf("failed to calculate total generation balance for height %d: %w", height, err)
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

totalGenerationBalance() has inverted error handling: it currently returns (0, nil) for unexpected errors and returns an error for ErrNoGeneratorsSet. This both hides real storage failures and incorrectly fails when the generators set simply doesn't exist; the logic should be the opposite (treat ErrNoGeneratorsSet as zero balance, and propagate other errors).

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.

Fixed.

Comment thread pkg/state/generators.go
Comment on lines +420 to +456
func (g *generatorsStorage) copyGeneratorsSetAndUpdateBalances(
pg *generatorsRecord, commitments []commitmentItem, threshold uint64, generatorPK crypto.PublicKey,
blockHeight, blockchainHeight proto.Height,
blockID proto.BlockID,
) error {
rec := generatorsRecord{
Generators: make([]generator, 0, len(pg.Generators)),
BlockGeneratorIndex: -1,
PeriodStart: pg.PeriodStart,
}
generatorsBalancesLSHRecord := newGeneratorsBalancesRecordForStateHashes(len(pg.Generators))
for i, gen := range pg.Generators {
ng := generator{
Balance: 0,
BanHeight: gen.BanHeight,
AddressID: gen.AddressID,
}
if gen.BanHeight == 0 { // The generator is not banned, query the balance.
// The initialization happens at the very beginning of the block, so the generation balance is
// queried at the height of the last applied block (blockchainHeight).
bal, bErr := g.balances.newestGeneratingBalance(gen.AddressID, blockchainHeight)
if bErr != nil {
return fmt.Errorf("failed to get balance for generator at index %d: %w", i, bErr)
}
if bal < threshold {
bal = 0 // Set to zero, generators with insufficient balance excluded from generators set.
}
ng.Balance = bal
}
if generatorPK == commitments[i].GeneratorPK {
// The block generator found.
idx, scErr := safecast.Convert[int32](i)
if scErr != nil {
return fmt.Errorf("failed to convert generator index: %w", scErr)
}
rec.BlockGeneratorIndex = idx
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

copyGeneratorsSetAndUpdateBalances() iterates over pg.Generators but uses commitments[i] to locate the block generator. If new commitments appear during the same generation period, this code won’t expand the generator set and can leave BlockGeneratorIndex unset (triggering an error in finishGeneratorsInitialization) and/or desync the stored generator record size from the current commitments list (breaking infos() which requires equal lengths). Consider rebuilding the record based on the current commitments length (carrying forward existing bans/balances where applicable) rather than freezing to the previous set size.

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.

New commitments is not possible for the current generation period. All commitments should be done before the period starts.

Comment thread pkg/state/generators.go
Comment on lines +132 to +138
func (r *generatorsRecord) banGenerator(index uint32, height proto.Height) error {
if s := len(r.Generators); int(index) >= s {
return fmt.Errorf("generator index %d is out of bounds for the generator set of size %d", index, s)
}
g := r.Generators[index]
if g.BanHeight != 0 && g.BanHeight <= height {
return fmt.Errorf("generator with index %d is already banned at height %d", index, g.BanHeight)
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

generatorsRecord.banGenerator() returns an error when the generator is already banned. Previously banning was idempotent (no-op if already banned), and processConflictingEndorsements() can plausibly encounter duplicate/conflicting entries for the same index within a block; returning an error here would abort finalization processing. Consider making this operation idempotent when BanHeight != 0 instead of failing.

Suggested change
func (r *generatorsRecord) banGenerator(index uint32, height proto.Height) error {
if s := len(r.Generators); int(index) >= s {
return fmt.Errorf("generator index %d is out of bounds for the generator set of size %d", index, s)
}
g := r.Generators[index]
if g.BanHeight != 0 && g.BanHeight <= height {
return fmt.Errorf("generator with index %d is already banned at height %d", index, g.BanHeight)
// If the generator is already banned, the operation is a no-op.
func (r *generatorsRecord) banGenerator(index uint32, height proto.Height) error {
if s := len(r.Generators); int(index) >= s {
return fmt.Errorf("generator index %d is out of bounds for the generator set of size %d", index, s)
}
g := r.Generators[index]
if g.BanHeight != 0 {
return nil

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.

No, I think that the error should be returned here, because a conflicting endorsement from the already banned generator should be considered as an invalid block.

Comment thread pkg/state/generators.go Outdated
Comment on lines +41 to +47
Index uint32 `json:"-"`
Address proto.WavesAddress `json:"address"`
PublicKey crypto.PublicKey
BLSPublicKey bls.PublicKey `json:"-"`
Balance uint64 `json:"balance"`
Ban bool
TransactionID crypto.Digest `json:"transactionID"`
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.

Please, add json tags for all fields.

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.

Comment thread pkg/state/balances.go
}

// burnDeposit reduces the balance of an account by Deposit amount. The Deposit part of balance profile is not affected.
func (s *balances) burnDeposit(addr proto.AddressID, blockID proto.BlockID) error {
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.

Imagine that bad generator has been punished ant its deposit has been burnt. Can the generator transfer all its funds to an another address including the Deposit part (i.e. old address balance will be set to zero)?

Comment thread pkg/state/balances.go
}
if sbErr := s.setWavesBalance(addr, v, blockID); sbErr != nil {
return fmt.Errorf("failed to burn deposit: %w", sbErr)
}
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.

Should WavesBalance snapshot be generated in the ephemeral next block/zero snapshot? This is a regular balance change.

Comment thread pkg/state/balances.go
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}
balance.Balance, err = common.SubInt(balance.Balance, Deposit)
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.

How the Deposit action affects generating balance? (only mining process or also Ride balances)

Comment on lines +960 to +963
m["getInteger"] = 7
m["getBoolean"] = 7
m["getBinary"] = 7
m["getString"] = 7
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.

Why 7? I've cheked json docs, it says that complexity equals 10, checked complexity in getDataByIndexF method and it equals 2.
P.S.: Values above are correct, no questions.

Comment thread pkg/node/fsm/ng_state.go Outdated
}

// 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.

Comment thread pkg/state/generators.go
type generationBalanceManager interface {
generationBalanceProvider
burnDeposit(proto.AddressID, proto.BlockID) error
}
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.

I think generationBalanceProvider can be deleted, because only its usage is this place.

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.

Removed.

Comment thread pkg/state/generators.go Outdated
Comment on lines +89 to +103
g := generators[i]
a, err := g.AddressID.ToWavesAddress(scheme)
if err != nil {
return nil, fmt.Errorf("failed to convert address ID to Waves address for generator %d: %w", i, err)
}
gi := GeneratorInfo{
Index: idx,
Address: a,
PublicKey: c.GeneratorPK,
BLSPublicKey: c.EndorserPK,
Balance: g.Balance,
Ban: g.BanHeight != 0,
TransactionID: c.TransactionID,
}
res[i] = gi
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.

Maybe replace to buildGeneratorInfo?

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, reused.

Comment thread pkg/state/generators.go Outdated
Comment on lines +108 to +109
type generatorsKey struct {
height uint64
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.

Suggest move to keys.go for unification.

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.

Moved

Comment thread pkg/state/generators.go Outdated
type bannedGeneratorsKey struct {
periodStart uint32
}
func (r *generatorsRecord) Size() int { return len(r.Generators) }
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.

Intentionally package-public?

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.

Nope, accidentally, made package private.

Comment thread pkg/state/generators.go
Comment on lines +152 to +171
sb.WriteRune('(')
sb.WriteString(strconv.Itoa(int(r.BlockGeneratorIndex)))
sb.WriteRune(')')
sb.WriteRune('[')
for i, gen := range r.Generators {
if i > 0 {
sb.WriteString(", ")
}
a, aErr := gen.AddressID.ToWavesAddress(scheme)
if aErr != nil {
return "", fmt.Errorf("failed to convert generator address ID to Waves address: %w", aErr)
}
sb.WriteString(a.String())
sb.WriteRune('(')
sb.WriteString(strconv.FormatUint(gen.Balance, 10))
sb.WriteRune(')')
}
sb.WriteRune(']')
sb.WriteRune('@')
sb.WriteString(strconv.Itoa(int(r.PeriodStart)))
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.

Suggest using fmt.Fprintf, improves readability on my mind.

Comment thread pkg/state/generators.go
return fmt.Errorf("failed to burn deposit of previous generator with index %d: %w",
i, bErr)
}
// TODO: Reset deposit here?
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.

Still TODO?

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, the place should be considered after some tests with Scala.

@alexeykiselev alexeykiselev deployed to Deploy-testnet-amd64 April 28, 2026 15:27 — with GitHub Actions Active
Comment thread pkg/proto/finalization.go
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.

err = to.balances.setWavesBalance(addr.ID(), newWavesValueFromProfile(tc.profile), blockID0)
require.NoError(t, err)
to.stor.flush(t)
err = to.balances.burnDeposit(addr.ID(), blockID1)
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.

Maybe also add assertion for expected profile/balance after the action?

Comment thread pkg/state/generators.go Outdated
if b < threshold {
b = 0 // Set generation balances less than threshold to zero, so they are not eligible for generation.
}
if generatorPK == commitments[i].GeneratorPK {
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.

Why not cm.GeneratorPK?

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.

Comment thread pkg/state/generators.go Outdated
g.set = make([]GeneratorInfo, 0, len(cms))
generatorsBalancesLSHRecord := newGeneratorsBalancesRecordForStateHashes(len(cms))
for i, cm := range cms {
generatorsBalancesLSHRecord := newGeneratorsBalancesRecordForStateHashes(len(commitments))
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.

No need to allocate this slice if g.calculateHashes == false

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.

Comment thread pkg/state/generators.go Outdated
BlockGeneratorIndex: -1,
PeriodStart: pg.PeriodStart,
}
generatorsBalancesLSHRecord := newGeneratorsBalancesRecordForStateHashes(len(pg.Generators))
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.

No need to allocate this slice if g.calculateHashes == false

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.

Comment thread pkg/state/generators.go
}
ng.Balance = bal
}
if generatorPK == commitments[i].GeneratorPK {
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.

Suggest to add sanity check len(commitments) >= len(pg.Generators)

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.

Added.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/proto/finalization.go
Comment on lines 112 to +116
type FinalizationVoting struct {
EndorserIndexes []uint32 `json:"endorserIndexes"`
FinalizedBlockHeight Height `json:"finalizedBlockHeight"`
EndorserIndexes []uint32 `json:"endorserIndexes,omitempty"`
FinalizedBlockHeight Height `json:"finalizedHeight"`
AggregatedEndorsementSignature bls.Signature `json:"aggregatedEndorsementSignature"`
ConflictEndorsements []BlockEndorsement `json:"conflictEndorsements"`
ConflictEndorsements []BlockEndorsement `json:"conflictEndorsements,omitempty"`
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.

Comment thread pkg/state/balances.go
Comment on lines +1017 to +1026
// burnDeposit reduces the balance of an account by Deposit amount. The Deposit part of balance profile is not affected.
func (s *balances) burnDeposit(addr proto.AddressID, blockID proto.BlockID) error {
balance, err := s.newestWavesBalance(addr)
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}
balance.Balance, err = common.SubInt(balance.Balance, Deposit)
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}
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.

burnDeposit() subtracts the fixed Deposit amount from balance.Balance but leaves balance.Deposit unchanged. Since spendable/effective balance calculations subtract Deposit again (see balanceProfile.effectiveBalanceUnchecked()), this can (a) double-penalize the account by 2*Deposit and (b) cause underflow errors on subsequent balance checks/transactions when Balance < Deposit after burning.

Consider burning from the locked deposit by decrementing both balance.Balance and balance.Deposit (after validating balance.Deposit >= Deposit) so the deposit lock is cleared immediately for punished generators. If you do that, resetDeposits() will also need to tolerate generators whose Deposit was already cleared (e.g., skip when Deposit < Deposit).

Suggested change
// burnDeposit reduces the balance of an account by Deposit amount. The Deposit part of balance profile is not affected.
func (s *balances) burnDeposit(addr proto.AddressID, blockID proto.BlockID) error {
balance, err := s.newestWavesBalance(addr)
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}
balance.Balance, err = common.SubInt(balance.Balance, Deposit)
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}
// burnDeposit reduces the balance of an account by Deposit amount and clears the corresponding locked deposit.
func (s *balances) burnDeposit(addr proto.AddressID, blockID proto.BlockID) error {
balance, err := s.newestWavesBalance(addr)
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}
if balance.Deposit < Deposit {
return fmt.Errorf("failed to burn deposit: insufficient locked deposit: have %d, need %d", balance.Deposit, Deposit)
}
balance.Balance, err = common.SubInt(balance.Balance, Deposit)
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}
balance.Deposit, err = common.SubInt(balance.Deposit, Deposit)
if err != nil {
return fmt.Errorf("failed to burn deposit: %w", err)
}

Copilot uses AI. Check for mistakes.
Comment thread pkg/state/generators.go
return fmt.Errorf("failed to burn deposit of previous generator with index %d: %w",
i, bErr)
}
// TODO: Reset deposit here?
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.

punish() burns the generator’s deposit but does not clear the generator’s locked Deposit in the balance profile (there’s a TODO for this). If burnDeposit() reduces only Balance while Deposit remains set, later spendable/effective balance computations can underflow or effectively penalize the account by more than the intended deposit amount.

It would be safer to clear/reset the deposit lock as part of punishment (either here or inside burnDeposit()), and ensure end-of-period deposit reset logic won’t fail for already-cleared deposits.

Suggested change
// TODO: Reset deposit here?
if rErr := g.balances.resetDeposit(cg.AddressID, blockID); rErr != nil {
return fmt.Errorf("failed to reset deposit of previous generator with index %d: %w",
i, rErr)
}

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +214
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
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.
Comment thread pkg/api/node_api.go
Comment on lines 927 to +931
gs, err := a.state.CommittedGenerators(height)
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)
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.
Comment thread pkg/proto/finalization.go
Comment on lines 60 to +62
EndorserIndex uint32 `json:"endorserIndex"`
FinalizedBlockID BlockID `json:"finalizedBlockID"`
FinalizedBlockHeight uint32 `json:"finalizedBlockHeight"`
FinalizedBlockID BlockID `json:"finalizedBlockId"`
FinalizedBlockHeight uint32 `json:"finalizedHeight"`
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.

Sync peer suspending added on block application error in NG state.
Sync peer suspending added on timeout and empty signatures reply in Sync state.
…height from the block itself.

FinalizationVoting validation improved. Tests fixed.
Endorsement generation for mined blocks simplified.
Function BuildLocalEndorsementMessage added to state API. Mocks updated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants