Skip to content
Draft
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ ALGOD_API_PACKAGES := $(sort $(shell GOPATH=$(GOPATH) && GO111MODULE=off && cd d

GOMOD_DIRS := ./tools/block-generator ./tools/x-repo-types

MSGP_GENERATE := ./protocol ./protocol/test ./crypto ./crypto/merklearray ./crypto/merklesignature ./crypto/stateproof ./data/basics ./data/transactions ./data/stateproofmsg ./data/committee ./data/bookkeeping ./data/hashable ./agreement ./rpcs ./network ./node ./ledger ./ledger/ledgercore ./ledger/store/trackerdb ./ledger/store/trackerdb/generickv ./ledger/encoded ./stateproof ./data/account ./daemon/algod/api/spec/v2
MSGP_GENERATE := ./protocol ./protocol/test ./crypto ./crypto/merklearray ./crypto/merklesignature ./crypto/stateproof ./data/basics ./data/transactions ./data/stateproofmsg ./data/committee ./data/bookkeeping ./data/hashable ./agreement ./rpcs ./network ./node ./ledger ./ledger/ledgercore ./ledger/statecommit ./ledger/store/trackerdb ./ledger/store/trackerdb/generickv ./ledger/encoded ./stateproof ./data/account ./daemon/algod/api/spec/v2

default: build

Expand Down
129 changes: 99 additions & 30 deletions agreement/msgp_gen.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions config/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,11 @@ type ConsensusParams struct {
// specify the current app. This parameter can be removed and assumed true
// after the first consensus release in which it is set true.
AllowZeroLocalAppRef bool

// EnableUpdateTrie enables the update trie commitment in block headers.
// When enabled, blocks include an UpdateCommitment hash that commits to
// the changes in the block.
EnableUpdateTrie bool
}

// ProposerPayoutRules puts several related consensus parameters in one place. The same
Expand Down Expand Up @@ -1461,6 +1466,7 @@ func initConsensusProtocols() {

vFuture.AppSizeUpdates = true
vFuture.AllowZeroLocalAppRef = true
vFuture.EnableUpdateTrie = true

Consensus[protocol.ConsensusFuture] = vFuture

Expand Down
4 changes: 4 additions & 0 deletions data/bookkeeping/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ type BlockHeader struct {
// TxnCommitments authenticates the set of transactions appearing in the block.
TxnCommitments

// UpdateCommitment is the root hash of the update trie for this block,
// committing to the sequence of state changes organized by transaction group.
UpdateCommitment crypto.Sha512Digest `codec:"uc"`

// TimeStamp in seconds since epoch
TimeStamp int64 `codec:"ts"`

Expand Down
86 changes: 66 additions & 20 deletions data/bookkeeping/msgp_gen.go

Large diffs are not rendered by default.

50 changes: 48 additions & 2 deletions ledger/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"github.com/algorand/go-algorand/ledger/apply"
"github.com/algorand/go-algorand/ledger/eval/prefetcher"
"github.com/algorand/go-algorand/ledger/ledgercore"
"github.com/algorand/go-algorand/ledger/statecommit"
"github.com/algorand/go-algorand/logging"
"github.com/algorand/go-algorand/protocol"
"github.com/algorand/go-algorand/util"
Expand Down Expand Up @@ -1945,6 +1946,38 @@
return nil
}

// updateCommitment computes the update commitment from the final state delta
func (eval *BlockEvaluator) updateCommitment() (ledgercore.StateDelta, error) {
stateDelta := eval.state.deltas()

if !eval.proto.EnableUpdateTrie {
// If update trie is not enabled, the commitment field must be empty
if eval.validate {
if eval.block.UpdateCommitment != (crypto.Sha512Digest{}) {
return ledgercore.StateDelta{}, fmt.Errorf("update commitment must be empty")
}
}
return stateDelta, nil
}

updateHash, err := statecommit.StateDeltaCommitment(&stateDelta)
if err != nil {
return ledgercore.StateDelta{}, fmt.Errorf("unable to compute update commitment: %w", err)
}

if eval.generate {
eval.block.UpdateCommitment = updateHash
}

if eval.validate {
if eval.block.UpdateCommitment != updateHash {
return ledgercore.StateDelta{}, fmt.Errorf("update commitment mismatch: expected %v, got %v", eval.block.UpdateCommitment, updateHash)
}
}

return stateDelta, nil
}

// GenerateBlock produces a complete block from the BlockEvaluator. This is
// used during proposal to get an actual block that will be proposed, after
// feeding in tentative transactions into this block evaluator.
Expand Down Expand Up @@ -1978,15 +2011,22 @@
// look up end-of-block state of possible proposers passed to GenerateBlock
finalAccounts := make(map[basics.Address]ledgercore.AccountData, len(participating))
for i := range participating {
acct, err := eval.state.lookup(participating[i])

Check failure on line 2014 in ledger/eval/eval.go

View workflow job for this annotation

GitHub Actions / reviewdog-errors

[Lint Errors] reported by reviewdog 🐶 shadow: declaration of "err" shadows declaration at line 2006 (govet) Raw Output: ledger/eval/eval.go:2014:9: shadow: declaration of "err" shadows declaration at line 2006 (govet) acct, err := eval.state.lookup(participating[i]) ^ 1 issues: * govet: 1

Check failure on line 2014 in ledger/eval/eval.go

View workflow job for this annotation

GitHub Actions / reviewdog-errors

shadow: declaration of "err" shadows declaration at line 2006 (govet)
if err != nil {
return nil, err
}
finalAccounts[participating[i]] = acct
}

vb := ledgercore.MakeUnfinishedBlock(eval.block, eval.state.deltas(), finalAccounts)
// Generate the update commitment from the final state delta
stateDelta, err := eval.updateCommitment()
if err != nil {
return nil, err
}

vb := ledgercore.MakeUnfinishedBlock(eval.block, stateDelta, finalAccounts)
eval.blockGenerated = true

proto, ok := config.Consensus[eval.block.BlockHeader.CurrentProtocol]
if !ok {
return nil, fmt.Errorf(
Expand Down Expand Up @@ -2188,6 +2228,12 @@
return ledgercore.StateDelta{}, err
}

// Generate or validate the update commitment from the final state delta
stateDelta, err := eval.updateCommitment()
if err != nil {
return ledgercore.StateDelta{}, err
}

// If validating, do final block checks that depend on our new state
if validate {
// wait for the signature validation to complete.
Expand All @@ -2204,5 +2250,5 @@
}
}

return eval.state.deltas(), nil
return stateDelta, nil
}
91 changes: 91 additions & 0 deletions ledger/statecommit/committer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (C) 2019-2025 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.

package statecommit

import (
"bytes"
"slices"

"github.com/algorand/go-algorand/crypto"
"github.com/algorand/go-algorand/crypto/merklearray"
"github.com/algorand/go-algorand/protocol"
)

// stateUpdate represents a single insert/update or delete operation
type stateUpdate struct {
_struct struct{} `codec:",omitempty,omitemptyarray"`

Key []byte `codec:"k"`
Value []byte `codec:"v"`
Deleted bool `codec:"d"`
Comment on lines +33 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't want to use nil Value as delete? I think we do that quite a bit elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't sure about nil vs empty for boxes and wanted to double check but it would be fine for the other types

}

// ToBeHashed implements crypto.Hashable for stateUpdate
func (u *stateUpdate) ToBeHashed() (protocol.HashID, []byte) {
return protocol.StateUpdateLeaf, protocol.Encode(u)
}

// merkleArrayCommitter implements UpdateCommitter using a Merkle array
type merkleArrayCommitter struct{ updates []stateUpdate }

// newMerkleArrayCommitter creates a new Merkle array-based update committer
func newMerkleArrayCommitter() UpdateCommitter { return &merkleArrayCommitter{} }

// updateArray implements merklearray.Array for stateUpdates
type updateArray struct{ updates []stateUpdate }

func (a *updateArray) Length() uint64 { return uint64(len(a.updates)) }
func (a *updateArray) Marshal(pos uint64) (crypto.Hashable, error) { return &a.updates[pos], nil }

// Add adds a key-value update
func (m *merkleArrayCommitter) Add(key, val []byte) error {
m.updates = append(m.updates, stateUpdate{Key: key, Value: val, Deleted: false})
return nil
}

// Delete adds a deletion update
func (m *merkleArrayCommitter) Delete(key []byte) error {
m.updates = append(m.updates, stateUpdate{Key: key, Value: nil, Deleted: true})
return nil
}

// Root returns the Merkle root commitment of all updates
func (m *merkleArrayCommitter) Root() (crypto.Sha512Digest, error) {
if len(m.updates) == 0 {
return crypto.Sha512Digest{}, nil
}

// Sort updates by key to ensure deterministic commitment (KvMods is a map)
slices.SortFunc(m.updates, func(a, b stateUpdate) int { return bytes.Compare(a.Key, b.Key) })

array := &updateArray{updates: m.updates}
// not calling merklearray.BuildVectorCommitmentTree (we don't want proof of position in array)
tree, err := merklearray.Build(array, crypto.HashFactory{HashType: crypto.Sha512})
if err != nil {
return crypto.Sha512Digest{}, err
}

rootSlice := tree.Root().ToSlice()
var root crypto.Sha512Digest
copy(root[:], rootSlice)
return root, nil
}

// Reset clears the committer state
func (m *merkleArrayCommitter) Reset() {
m.updates = nil
}
151 changes: 151 additions & 0 deletions ledger/statecommit/deltatrie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright (C) 2019-2025 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.

package statecommit

import (
"github.com/algorand/go-algorand/crypto"
"github.com/algorand/go-algorand/data/basics"
"github.com/algorand/go-algorand/ledger/ledgercore"
"github.com/algorand/go-algorand/protocol"
)

// stateChange represents any type of state change that can be encoded into a trie update.
// This interface abstracts over accounts, resources (assets/apps), and key-value pairs.
type stateChange interface {
isDeleted() bool
hasNewValue() bool
encodeKey() []byte
encodeValue() []byte
}

// Wrapper types to allow various delta types to implement the stateChange interface.
// Each wrapper provides methods for encoding one specific delta type.

//msgp:ignore accountUpdate
type accountUpdate ledgercore.BalanceRecord

func (u *accountUpdate) isDeleted() bool { return u.AccountData.IsZero() }
func (u *accountUpdate) hasNewValue() bool { return !u.AccountData.IsZero() }
func (u *accountUpdate) encodeKey() []byte { return EncodeAccountKey(u.Addr) }
func (u *accountUpdate) encodeValue() []byte {
// encode using codec tags on basics.AccountData
var ad basics.AccountData
ledgercore.AssignAccountData(&ad, u.AccountData)
return protocol.Encode(&ad)
}

//msgp:ignore kvUpdate
type kvUpdate struct {
key *string
delta *ledgercore.KvValueDelta
}

func (u *kvUpdate) isDeleted() bool { return u.delta.Data == nil }
func (u *kvUpdate) hasNewValue() bool { return u.delta.Data != nil }
func (u *kvUpdate) encodeKey() []byte { return EncodeKvPairKey(*u.key) }
func (u *kvUpdate) encodeValue() []byte { return u.delta.Data } // XXX need to distinguish between nil and empty?

//msgp:ignore assetHoldingUpdate
type assetHoldingUpdate ledgercore.AssetResourceRecord

func (u *assetHoldingUpdate) isDeleted() bool { return u.Holding.Deleted }
func (u *assetHoldingUpdate) hasNewValue() bool { return u.Holding.Holding != nil }
func (u *assetHoldingUpdate) encodeKey() []byte { return EncodeAssetHoldingKey(u.Addr, u.Aidx) }
func (u *assetHoldingUpdate) encodeValue() []byte { return protocol.Encode(u.Holding.Holding) }

//msgp:ignore assetParamsUpdate
type assetParamsUpdate ledgercore.AssetResourceRecord

func (u *assetParamsUpdate) isDeleted() bool { return u.Params.Deleted }
func (u *assetParamsUpdate) hasNewValue() bool { return u.Params.Params != nil }
func (u *assetParamsUpdate) encodeKey() []byte { return EncodeAssetParamsKey(u.Addr, u.Aidx) }
func (u *assetParamsUpdate) encodeValue() []byte { return protocol.Encode(u.Params.Params) }

//msgp:ignore appLocalStateUpdate
type appLocalStateUpdate ledgercore.AppResourceRecord

func (u *appLocalStateUpdate) isDeleted() bool { return u.State.Deleted }
func (u *appLocalStateUpdate) hasNewValue() bool { return u.State.LocalState != nil }
func (u *appLocalStateUpdate) encodeKey() []byte { return EncodeAppLocalStateKey(u.Addr, u.Aidx) }
func (u *appLocalStateUpdate) encodeValue() []byte { return protocol.Encode(u.State.LocalState) }

//msgp:ignore appParamsUpdate
type appParamsUpdate ledgercore.AppResourceRecord

func (u *appParamsUpdate) isDeleted() bool { return u.Params.Deleted }
func (u *appParamsUpdate) hasNewValue() bool { return u.Params.Params != nil }
func (u *appParamsUpdate) encodeKey() []byte { return EncodeAppParamsKey(u.Addr, u.Aidx) }
func (u *appParamsUpdate) encodeValue() []byte { return protocol.Encode(u.Params.Params) }

// maybeCommitUpdate checks if an update has changes and adds it to the committer
func maybeCommitUpdate[T stateChange](update T, committer UpdateCommitter) error {
if update.isDeleted() {
return committer.Delete(update.encodeKey())
}
if update.hasNewValue() {
return committer.Add(update.encodeKey(), update.encodeValue())
}
return nil
}

// StateDeltaCommitment computes a cryptographic commitment to all state changes in a StateDelta.
// This is the primary function for converting ledger state changes into a state commitment.
func StateDeltaCommitment(sd *ledgercore.StateDelta) (crypto.Sha512Digest, error) {
return stateDeltaCommitmentWithCommitter(sd, newMerkleArrayCommitter())
}

// stateDeltaCommitmentWithCommitter computes a cryptographic commitment using the provided UpdateCommitter.
// This allows flexibility in the commitment scheme used.
func stateDeltaCommitmentWithCommitter(sd *ledgercore.StateDelta, committer UpdateCommitter) (crypto.Sha512Digest, error) {
// Process base account data changes
for i := range sd.Accts.Accts {
if err := maybeCommitUpdate((*accountUpdate)(&sd.Accts.Accts[i]), committer); err != nil {
return crypto.Sha512Digest{}, err
}
}

// Process asset resources (holdings and params separately)
for i := range sd.Accts.AssetResources {
rec := &sd.Accts.AssetResources[i]
if err := maybeCommitUpdate((*assetHoldingUpdate)(rec), committer); err != nil {
return crypto.Sha512Digest{}, err
}
if err := maybeCommitUpdate((*assetParamsUpdate)(rec), committer); err != nil {
return crypto.Sha512Digest{}, err
}
}

// Process app resources (local state and params separately)
for i := range sd.Accts.AppResources {
rec := &sd.Accts.AppResources[i]
if err := maybeCommitUpdate((*appLocalStateUpdate)(rec), committer); err != nil {
return crypto.Sha512Digest{}, err
}
if err := maybeCommitUpdate((*appParamsUpdate)(rec), committer); err != nil {
return crypto.Sha512Digest{}, err
}
}

// Process KV modifications
for key, kvDelta := range sd.KvMods {
if err := maybeCommitUpdate(&kvUpdate{&key, &kvDelta}, committer); err != nil {
return crypto.Sha512Digest{}, err
}
}

return committer.Root()
}
Loading
Loading