Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
102 changes: 76 additions & 26 deletions pkg/proto/legacy_state_hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,63 @@ func EmptyLegacyStateHash(finalityActivated bool) StateHash {
return &StateHashV1{}
}

type LegacyStateHashParams struct {
v2Params struct {
areSet bool
generatorsHash crypto.Digest
generatorsBalancesHash crypto.Digest
}
}

type LegacyStateHashOption func(*LegacyStateHashParams)

func LegacyStateHashV2Opt(generatorsHash, generatorsBalancesHash crypto.Digest) LegacyStateHashOption {
return func(params *LegacyStateHashParams) {
params.v2Params = struct {
areSet bool
generatorsHash crypto.Digest
generatorsBalancesHash crypto.Digest
}{
areSet: true,
generatorsHash: generatorsHash,
generatorsBalancesHash: generatorsBalancesHash,
}
}
}

type LegacyStateHashFeatureActivated struct {
_ struct{}
FinalityActivated bool
}

// NewLegacyStateHash creates a new legacy StateHash depending on whether
// the Deterministic Finality feature is activated.
// If generatorsHash in not provided but finalityActivated is true, it will be set to zero value.
// When finality is activated, both generatorsHash and generatorsBalancesHash must be provided
// via LegacyStateHashV2Opt, otherwise NewLegacyStateHash returns an error.
func NewLegacyStateHash(
finalityActivated bool, blockID BlockID, fh FieldsHashesV1, generatorsHash ...crypto.Digest,
) StateHash {
if finalityActivated {
var gh crypto.Digest
if len(generatorsHash) > 0 {
gh = generatorsHash[0]
blockID BlockID, fh FieldsHashesV1, f LegacyStateHashFeatureActivated, option ...LegacyStateHashOption,
) (StateHash, error) {
var p LegacyStateHashParams
for _, opt := range option {
opt(&p)
}
if f.FinalityActivated {
if !p.v2Params.areSet {
return nil, fmt.Errorf("params for StateHashV2 are not set, but finality feature is activated")
}
return &StateHashV2{
BlockID: blockID,
FieldsHashesV2: FieldsHashesV2{
FieldsHashesV1: fh,
GeneratorsHash: gh,
FieldsHashesV1: fh,
GeneratorsHash: p.v2Params.generatorsHash,
GeneratorsBalancesHash: p.v2Params.generatorsBalancesHash,
},
}
}, nil
}
return &StateHashV1{
BlockID: blockID,
FieldsHashesV1: fh,
}
}, nil
}

// FieldsHashesV1 is set of hashes fields for the legacy StateHashV1.
Expand Down Expand Up @@ -200,14 +234,16 @@ func (s *FieldsHashesV1) ReadFrom(r io.Reader) (int64, error) {
}

// FieldsHashesV2 is set of hashes fields for the legacy StateHashV2.
// It's a FieldsHashesV1 with an additional GeneratorsHash field.
// It's a FieldsHashesV1 with additional GeneratorsHash and GeneratorsBalancesHash fields.
type FieldsHashesV2 struct {
FieldsHashesV1
GeneratorsHash crypto.Digest
GeneratorsHash crypto.Digest
GeneratorsBalancesHash crypto.Digest
}

func (s *FieldsHashesV2) Equal(other FieldsHashesV2) bool {
return s.FieldsHashesV1.Equal(other.FieldsHashesV1) && s.GeneratorsHash == other.GeneratorsHash
return s.FieldsHashesV1.Equal(other.FieldsHashesV1) && s.GeneratorsHash == other.GeneratorsHash &&
s.GeneratorsBalancesHash == other.GeneratorsBalancesHash
}

func (s FieldsHashesV2) MarshalJSON() ([]byte, error) {
Expand All @@ -223,7 +259,8 @@ func (s FieldsHashesV2) MarshalJSON() ([]byte, error) {
SponsorshipHash: DigestWrapped(s.SponsorshipHash),
AliasesHash: DigestWrapped(s.AliasesHash),
},
GeneratorsHash: DigestWrapped(s.GeneratorsHash),
GeneratorsHash: DigestWrapped(s.GeneratorsHash),
GeneratorsBalancesHash: DigestWrapped(s.GeneratorsBalancesHash),
})
}

Expand All @@ -242,25 +279,34 @@ func (s *FieldsHashesV2) UnmarshalJSON(value []byte) error {
s.SponsorshipHash = crypto.Digest(sh.SponsorshipHash)
s.AliasesHash = crypto.Digest(sh.AliasesHash)
s.GeneratorsHash = crypto.Digest(sh.GeneratorsHash)
s.GeneratorsBalancesHash = crypto.Digest(sh.GeneratorsBalancesHash)
return nil
}

func (s *FieldsHashesV2) WriteTo(w io.Writer) (int64, error) {
n, err := s.FieldsHashesV1.WriteTo(w)
fh1Size, err := s.FieldsHashesV1.WriteTo(w)
if err != nil {
return fh1Size, err
}
ghSize, err := w.Write(s.GeneratorsHash[:])
if err != nil {
return n, err
return fh1Size + int64(ghSize), err
}
m, err := w.Write(s.GeneratorsHash[:])
return n + int64(m), err
gbhSize, err := w.Write(s.GeneratorsBalancesHash[:])
return fh1Size + int64(ghSize) + int64(gbhSize), err
}

func (s *FieldsHashesV2) ReadFrom(r io.Reader) (int64, error) {
n, err := s.FieldsHashesV1.ReadFrom(r)
fh1Size, err := s.FieldsHashesV1.ReadFrom(r)
if err != nil {
return fh1Size, err
}
ghSize, err := io.ReadFull(r, s.GeneratorsHash[:])
if err != nil {
return n, err
return fh1Size + int64(ghSize), err
}
m, err := io.ReadFull(r, s.GeneratorsHash[:])
return n + int64(m), err
gbhSize, err := io.ReadFull(r, s.GeneratorsBalancesHash[:])
return fh1Size + int64(ghSize) + int64(gbhSize), err
}

// StateHashV1 is the legacy state hash structure used prior the activation of Deterministic Finality feature.
Expand Down Expand Up @@ -430,6 +476,7 @@ func (s *StateHashV2) UnmarshalJSON(value []byte) error {
s.AssetBalanceHash = crypto.Digest(sh.AssetBalanceHash)
s.LeaseBalanceHash = crypto.Digest(sh.LeaseBalanceHash)
s.GeneratorsHash = crypto.Digest(sh.GeneratorsHash)
s.GeneratorsBalancesHash = crypto.Digest(sh.GeneratorsBalancesHash)
return nil
}

Expand Down Expand Up @@ -486,7 +533,8 @@ func (s *StateHashV2) toStateHashJS() stateHashJSV2 {
SponsorshipHash: DigestWrapped(s.SponsorshipHash),
AliasesHash: DigestWrapped(s.AliasesHash),
},
GeneratorsHash: DigestWrapped(s.GeneratorsHash),
GeneratorsHash: DigestWrapped(s.GeneratorsHash),
GeneratorsBalancesHash: DigestWrapped(s.GeneratorsBalancesHash),
},
}
}
Expand Down Expand Up @@ -607,7 +655,8 @@ func (s StateHashDebugV2) GetStateHash() StateHash {
SponsorshipHash: crypto.Digest(s.SponsorshipHash),
AliasesHash: crypto.Digest(s.AliasesHash),
},
GeneratorsHash: crypto.Digest(s.GeneratorsHash),
GeneratorsHash: crypto.Digest(s.GeneratorsHash),
GeneratorsBalancesHash: crypto.Digest(s.GeneratorsBalancesHash),
},
}
return sh
Expand Down Expand Up @@ -659,7 +708,8 @@ type fieldsHashesJSV1 struct {

type fieldsHashesJSV2 struct {
fieldsHashesJSV1
GeneratorsHash DigestWrapped `json:"nextCommittedGeneratorsHash"`
GeneratorsHash DigestWrapped `json:"nextCommittedGeneratorsHash"`
GeneratorsBalancesHash DigestWrapped `json:"committedGeneratorBalancesHash"`
}

type stateHashJSV1 struct {
Expand Down
33 changes: 27 additions & 6 deletions pkg/proto/legacy_state_hash_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,13 @@ func TestStateHashBinaryRoundTrip(t *testing.T) {
for i := range 10 {
t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
activated := randomBool()
sh := NewLegacyStateHash(activated, randomBlockID(), randomFieldsHashesV1(), randomDigest())
sh, err := NewLegacyStateHash(randomBlockID(), randomFieldsHashesV1(),
LegacyStateHashFeatureActivated{
FinalityActivated: activated,
},
LegacyStateHashV2Opt(randomDigest(), randomDigest()),
)
require.NoError(t, err)
data, err := sh.MarshalBinary()
require.NoError(t, err)
sh2 := EmptyLegacyStateHash(activated)
Expand All @@ -184,7 +190,13 @@ func TestStateHashJSONRoundTrip(t *testing.T) {
for i := range 10 {
t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
activated := randomBool()
sh := NewLegacyStateHash(activated, randomBlockID(), randomFieldsHashesV1(), randomDigest())
sh, err := NewLegacyStateHash(randomBlockID(), randomFieldsHashesV1(),
LegacyStateHashFeatureActivated{
FinalityActivated: activated,
},
LegacyStateHashV2Opt(randomDigest(), randomDigest()),
)
require.NoError(t, err)
js, err := sh.MarshalJSON()
require.NoError(t, err)
sh2 := EmptyLegacyStateHash(activated)
Expand All @@ -207,7 +219,7 @@ func TestStateHash_GenerateSumHash(t *testing.T) {
func TestStateHashV2_GenerateSumHashScalaCompatibility(t *testing.T) {
/* Output from Scala test com/wavesplatform/state/StateHashSpec.scala:138
PrevHash: 46e2hSbVy6YNqx4GH2ZwJW66jMD6FgXzirAUHDD6mVGi
StateHash: StateHash(3jiGZ5Wiyhm2tubLEgWgnh5eSSjJQqRnTXtMXE2y5HL8,
StateHash: StateHash(KdA4trKip6EpfUSzca42sLVqqjuHishcHDQZeYDC1Mo,
HashMap(
WavesBalance -> 3PhZ3CqdvDR58QGE62gVJFm5pZ6Q5CMpSLWV3KxVkAT7,
LeaseBalance -> 59QG6ZmcCkLmNuuPLxp2ifNZcr4BzMCahtKQ5iqyM1kJ,
Expand All @@ -219,9 +231,10 @@ func TestStateHashV2_GenerateSumHashScalaCompatibility(t *testing.T) {
AccountScript -> AMrxWar34wJdGWjDj2peT2c1itiPaPwY81hU32hyrB88,
Alias -> 46e2hSbVy6YNqx4GH2ZwJW66jMD6FgXzirAUHDD6mVGi,
AssetScript -> H8V5TrNNmwCU1erqVXmQbLoi9b4kd5iJSpMmvJ7CXeyf
CommittedGeneratorBalances -> EUKq8xDt8hyATpY6mmPev2bVjVmJAFQzXdTVyky34CEr
)
)
TotalHash: 3jiGZ5Wiyhm2tubLEgWgnh5eSSjJQqRnTXtMXE2y5HL8
TotalHash: KdA4trKip6EpfUSzca42sLVqqjuHishcHDQZeYDC1Mo
*/
sh := StateHashV2{
FieldsHashesV2: FieldsHashesV2{
Expand All @@ -237,10 +250,12 @@ func TestStateHashV2_GenerateSumHashScalaCompatibility(t *testing.T) {
AliasesHash: crypto.MustDigestFromBase58("46e2hSbVy6YNqx4GH2ZwJW66jMD6FgXzirAUHDD6mVGi"),
},
GeneratorsHash: crypto.MustDigestFromBase58("Gni1oXsHrtK8wSEuRDeZ9qpF8UpKj41HGEWaYSj9bCyC"),
// EUKq8xDt8hyATpY6mmPev2bVjVmJAFQzXdTVyky34CEr
GeneratorsBalancesHash: crypto.MustFastHash(binary.BigEndian.AppendUint64(nil, 3000)),
},
}
prevHash := crypto.MustDigestFromBase58("46e2hSbVy6YNqx4GH2ZwJW66jMD6FgXzirAUHDD6mVGi")
correctSumHash := crypto.MustDigestFromBase58("3jiGZ5Wiyhm2tubLEgWgnh5eSSjJQqRnTXtMXE2y5HL8")
correctSumHash := crypto.MustDigestFromBase58("KdA4trKip6EpfUSzca42sLVqqjuHishcHDQZeYDC1Mo")
err := sh.GenerateSumHash(prevHash.Bytes())
require.NoError(t, err)
assert.Equal(t, correctSumHash, sh.GetSumHash())
Expand All @@ -250,7 +265,13 @@ func TestStateHashDebug(t *testing.T) {
for i := range 10 {
t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
activated := randomBool()
sh := NewLegacyStateHash(activated, randomBlockID(), randomFieldsHashesV1(), randomDigest())
sh, err := NewLegacyStateHash(randomBlockID(), randomFieldsHashesV1(),
LegacyStateHashFeatureActivated{
FinalityActivated: activated,
},
LegacyStateHashV2Opt(randomDigest(), randomDigest()),
)
require.NoError(t, err)
h := randomHeight()
v := randomVersion()
ss := randomDigest()
Expand Down
15 changes: 15 additions & 0 deletions pkg/state/commitments.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/ccoveille/go-safecast/v2"
"github.com/fxamacker/cbor/v2"
"github.com/pkg/errors"

"github.com/wavesplatform/gowaves/pkg/crypto"
"github.com/wavesplatform/gowaves/pkg/crypto/bls"
"github.com/wavesplatform/gowaves/pkg/keyvalue"
Expand Down Expand Up @@ -449,3 +450,17 @@ func (c *commitments) removeGenerator(

return nil
}

func (c *commitments) prepareHashes() error {
if !c.calculateHashes {
return nil // No-op if hash calculation is disabled.
}
return c.hasher.stop()
}

func (c *commitments) reset() {
if !c.calculateHashes {
return // No-op if hash calculation is disabled.
}
c.hasher.reset()
}
55 changes: 52 additions & 3 deletions pkg/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package state
import (
"bytes"
"context"
"encoding/binary"
stderrs "errors"
"fmt"
"io"
Expand Down Expand Up @@ -74,6 +75,7 @@ type blockchainEntitiesStorage struct {
patches *patchesStorage
commitments *commitments
finalizations *finalizations
settings *settings.BlockchainSettings
calculateHashes bool
}

Expand Down Expand Up @@ -110,14 +112,48 @@ func newBlockchainEntitiesStorage(hs *historyStorage, sets *settings.BlockchainS
newPatchesStorage(hs, sets.AddressSchemeCharacter),
newCommitments(hs, calcHashes),
newFinalizations(hs),
sets,
calcHashes,
}, nil
}

func calculateCommittedGeneratorsBalancesStateHash(
s *blockchainEntitiesStorage, finalityActivated bool, blockHeight proto.Height,
) (crypto.Digest, error) {
generatorsBalancesSH := s.commitments.hasher.emptyHash // take empty hash from commitments record
if !finalityActivated {
return generatorsBalancesSH, nil // not activated, return empty hash
}
finalityActivationHeight, err := s.features.newestActivationHeight(int16(settings.DeterministicFinality))
if err != nil {
return crypto.Digest{}, fmt.Errorf("failed to get finality activation height: %w", err)
}
period, err := CurrentGenerationPeriodStart(finalityActivationHeight, blockHeight, s.settings.GenerationPeriod)
if err != nil {
return crypto.Digest{}, fmt.Errorf("failed to get current generation period start: %w", err)
}
// generators are sorted by their commitment index, so the balances will be in the same order.
generators, err := s.commitments.CommittedGeneratorsAddresses(period, s.settings.AddressSchemeCharacter)
if err != nil {
return crypto.Digest{}, fmt.Errorf("failed to get committed generators addresses: %w", err)
}
generatorsBalancesRecord := make([]byte, 0, len(generators)*uint64Size)
for _, addr := range generators {
bal, bErr := s.balances.newestGeneratingBalance(addr.ID(), blockHeight)
if bErr != nil {
return crypto.Digest{}, fmt.Errorf("failed to get generating balance for address %s: %w",
addr.String(), bErr,
)
}
generatorsBalancesRecord = binary.BigEndian.AppendUint64(generatorsBalancesRecord, bal)
}
return crypto.FastHash(generatorsBalancesRecord)
Comment on lines +136 to +150
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

When there are no committed generators for the current period, this function will hash an empty byte slice. Consider whether this is the intended behavior or if an error should be returned when len(generators) == 0, since having no committed generators in an active finality period might indicate a system state issue. If hashing an empty slice is correct, consider adding a comment explaining this behavior.

Copilot uses AI. Check for mistakes.
}

func (s *blockchainEntitiesStorage) putStateHash(
prevHash []byte, height uint64, blockID proto.BlockID,
) (proto.StateHash, error) {
finalityActivated := s.features.isActivatedAtHeight(int16(settings.DeterministicFinality), height)
finalityActivated := s.features.newestIsActivatedAtHeight(int16(settings.DeterministicFinality), height)
fhV1 := proto.FieldsHashesV1{
WavesBalanceHash: s.balances.wavesHashAt(blockID),
AssetBalanceHash: s.balances.assetsHashAt(blockID),
Expand All @@ -129,7 +165,19 @@ func (s *blockchainEntitiesStorage) putStateHash(
SponsorshipHash: s.sponsoredAssets.hasher.stateHashAt(blockID),
AliasesHash: s.aliases.hasher.stateHashAt(blockID),
}
sh := proto.NewLegacyStateHash(finalityActivated, blockID, fhV1, s.commitments.hasher.stateHashAt(blockID))
generatorsBalancesSH, err := calculateCommittedGeneratorsBalancesStateHash(s, finalityActivated, height)
if err != nil {
return nil, err
}
sh, shErr := proto.NewLegacyStateHash(blockID, fhV1,
proto.LegacyStateHashFeatureActivated{
FinalityActivated: finalityActivated,
},
proto.LegacyStateHashV2Opt(s.commitments.hasher.stateHashAt(blockID), generatorsBalancesSH),
)
if shErr != nil {
return nil, shErr
}
if gErr := sh.GenerateSumHash(prevHash); gErr != nil {
return nil, gErr
}
Expand Down Expand Up @@ -158,7 +206,7 @@ func (s *blockchainEntitiesStorage) prepareHashes() error {
if err := s.aliases.prepareHashes(); err != nil {
return err
}
return nil
return s.commitments.prepareHashes()
}

func (s *blockchainEntitiesStorage) handleLegacyStateHashes(blockchainHeight uint64, blockIds []proto.BlockID) error {
Expand Down Expand Up @@ -224,6 +272,7 @@ func (s *blockchainEntitiesStorage) reset() {
s.leases.reset()
s.sponsoredAssets.reset()
s.aliases.reset()
s.commitments.reset()
}

func (s *blockchainEntitiesStorage) flush() error {
Expand Down
Loading
Loading