Burn deposit#2070
Conversation
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.
Functions complexities updated for RideV9. Test on RideV9 estimation added. Logging improved.
There was a problem hiding this comment.
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
TransactionIDand propagate the newstore(..., 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.
| 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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed by calling GenerateID before accessing ID field. GenerateID() is no-op if a transaction already has ID.
| // 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 |
There was a problem hiding this comment.
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).
| 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())) |
There was a problem hiding this comment.
Log field key blockHeigh looks like a typo; should likely be blockHeight for consistency with other structured logs.
| slog.Uint64("blockHeigh", height), slog.String("blockID", blockID.String())) | |
| slog.Uint64("blockHeight", height), slog.String("blockID", blockID.String())) |
| rec.append(generatorPK, endorserPK, txID) | ||
| newData, mErr := rec.marshalBinary() | ||
| if mErr != nil { | ||
| return fmt.Errorf("failed to marshal commitments record: %w", mErr) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
No, transaction ID is not included in the legacy state hash. We store transactions IDs for information purposes of API only.
| 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yes, but there is special case in the same switch for such cases.
| 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) | ||
| } |
There was a problem hiding this comment.
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).
| 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 | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
New commitments is not possible for the current generation period. All commitments should be done before the period starts.
| 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) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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"` |
There was a problem hiding this comment.
Please, add json tags for all fields.
| } | ||
|
|
||
| // 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 { |
There was a problem hiding this comment.
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)?
| } | ||
| if sbErr := s.setWavesBalance(addr, v, blockID); sbErr != nil { | ||
| return fmt.Errorf("failed to burn deposit: %w", sbErr) | ||
| } |
There was a problem hiding this comment.
Should WavesBalance snapshot be generated in the ephemeral next block/zero snapshot? This is a regular balance change.
| if err != nil { | ||
| return fmt.Errorf("failed to burn deposit: %w", err) | ||
| } | ||
| balance.Balance, err = common.SubInt(balance.Balance, Deposit) |
There was a problem hiding this comment.
How the Deposit action affects generating balance? (only mining process or also Ride balances)
| m["getInteger"] = 7 | ||
| m["getBoolean"] = 7 | ||
| m["getBinary"] = 7 | ||
| m["getString"] = 7 |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| // Following variables are used for logging only. | ||
| var activationHeight proto.Height |
There was a problem hiding this comment.
This variable declaration can be moved inside if block.
| type generationBalanceManager interface { | ||
| generationBalanceProvider | ||
| burnDeposit(proto.AddressID, proto.BlockID) error | ||
| } |
There was a problem hiding this comment.
I think generationBalanceProvider can be deleted, because only its usage is this place.
| 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 |
There was a problem hiding this comment.
Maybe replace to buildGeneratorInfo?
There was a problem hiding this comment.
Yes, reused.
| type generatorsKey struct { | ||
| height uint64 |
There was a problem hiding this comment.
Suggest move to keys.go for unification.
| type bannedGeneratorsKey struct { | ||
| periodStart uint32 | ||
| } | ||
| func (r *generatorsRecord) Size() int { return len(r.Generators) } |
There was a problem hiding this comment.
Intentionally package-public?
There was a problem hiding this comment.
Nope, accidentally, made package private.
| 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))) |
There was a problem hiding this comment.
Suggest using fmt.Fprintf, improves readability on my mind.
| return fmt.Errorf("failed to burn deposit of previous generator with index %d: %w", | ||
| i, bErr) | ||
| } | ||
| // TODO: Reset deposit here? |
There was a problem hiding this comment.
Yes, the place should be considered after some tests with Scala.
| type FinalizationVoting struct { | ||
| EndorserIndexes []uint32 `json:"endorserIndexes"` | ||
| FinalizedBlockHeight Height `json:"finalizedBlockHeight"` | ||
| EndorserIndexes []uint32 `json:"endorserIndexes,omitempty"` |
There was a problem hiding this comment.
nil slice -> null, empty non nil slice -> []. Is it intended behavior for EndorserIndexes and ConflictEndorsements?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Maybe also add assertion for expected profile/balance after the action?
| 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 { |
There was a problem hiding this comment.
Why not cm.GeneratorPK?
| g.set = make([]GeneratorInfo, 0, len(cms)) | ||
| generatorsBalancesLSHRecord := newGeneratorsBalancesRecordForStateHashes(len(cms)) | ||
| for i, cm := range cms { | ||
| generatorsBalancesLSHRecord := newGeneratorsBalancesRecordForStateHashes(len(commitments)) |
There was a problem hiding this comment.
No need to allocate this slice if g.calculateHashes == false
| BlockGeneratorIndex: -1, | ||
| PeriodStart: pg.PeriodStart, | ||
| } | ||
| generatorsBalancesLSHRecord := newGeneratorsBalancesRecordForStateHashes(len(pg.Generators)) |
There was a problem hiding this comment.
No need to allocate this slice if g.calculateHashes == false
| } | ||
| ng.Balance = bal | ||
| } | ||
| if generatorPK == commitments[i].GeneratorPK { |
There was a problem hiding this comment.
Suggest to add sanity check len(commitments) >= len(pg.Generators)
There was a problem hiding this comment.
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.
| 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"` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
It's ok, no release yet.
| // 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) | ||
| } |
There was a problem hiding this comment.
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).
| // 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) | |
| } |
| return fmt.Errorf("failed to burn deposit of previous generator with index %d: %w", | ||
| i, bErr) | ||
| } | ||
| // TODO: Reset deposit here? |
There was a problem hiding this comment.
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.
| // 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) | |
| } |
| 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 |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
GeneratorsAt now returns []state.GeneratorInfo directly. This changes the public JSON shape compared to the previous response wrapper:
- additional fields like
PublicKeyandBanwill 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.
| EndorserIndex uint32 `json:"endorserIndex"` | ||
| FinalizedBlockID BlockID `json:"finalizedBlockID"` | ||
| FinalizedBlockHeight uint32 `json:"finalizedBlockHeight"` | ||
| FinalizedBlockID BlockID `json:"finalizedBlockId"` | ||
| FinalizedBlockHeight uint32 `json:"finalizedHeight"` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
No description provided.