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
10 changes: 5 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Set up Go environment
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: '1.26'
- name: Tidy go module
run: |
go mod tidy
Expand All @@ -40,7 +40,7 @@ jobs:
fi
- name: Run gofumpt
# Run gofumpt first, as it's quick and issues are common.
run: diff -u <(echo -n) <(go run mvdan.cc/gofumpt@v0.7.0 -d .)
run: diff -u <(echo -n) <(go run mvdan.cc/gofumpt@v0.9.2 -d .)
- name: Run go vet
run: go vet ./...
- name: Run go generate
Expand Down Expand Up @@ -81,7 +81,7 @@ jobs:
- uses: benjlevesque/short-sha@v3.0 # sets env.SHA to the first 7 chars of github.sha
- uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: '1.26'
- run: mkdir -p "$PWD/gocoverage-unit/"
- name: Run Go test -race
id: go-test-race
Expand Down Expand Up @@ -151,7 +151,7 @@ jobs:
- uses: actions/download-artifact@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: '1.26'
cache: false
- name: Convert gocoverage format
run: |
Expand Down Expand Up @@ -190,7 +190,7 @@ jobs:
name: gocoverage-all-textfmt@${{ env.SHA }}
- uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: '1.26'
cache: false
- name: Send coverage to coveralls.io (unit)
if: ${{ always() }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
path: developer-portal
- uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: '1.26'
cache: false
- name: Install swag
run: |
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:experimental

FROM golang:1.24 AS builder
FROM golang:1.26 AS builder

ARG BUILDARGS

Expand Down
2 changes: 1 addition & 1 deletion api/api_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ func CensusTypeToOrigin(ctype CensusTypeDescription) (models.CensusOrigin, []byt
origin = models.CensusOrigin_FARCASTER_FRAME
root = ctype.RootHash
default:
return 0, nil, ErrCensusTypeUnknown.Withf("%q", ctype)
return 0, nil, ErrCensusTypeUnknown.Withf("%v", ctype)
}
if root == nil {
return 0, nil, ErrCensusRootIsNil
Expand Down
16 changes: 15 additions & 1 deletion cmd/voconed/voconed.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"encoding/hex"
"fmt"
"os"
Expand Down Expand Up @@ -226,7 +227,14 @@ func main() {
vc.App.SetBlockTimeTarget(time.Second * time.Duration(config.blockSeconds))
vc.SetBlockSize(config.blockSize)

go vc.Start()
ctx, cancel := context.WithCancel(context.Background())
startDone := make(chan struct{})
go func() {
defer close(startDone)
if err := vc.Start(ctx); err != nil {
log.Fatal(err)
}
}()
uAPI, err := vc.EnableAPI("0.0.0.0", config.port, config.path)
if err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -264,4 +272,10 @@ func main() {
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
log.Warnf("received SIGTERM, exiting at %s", time.Now().Format(time.RFC850))
cancel()
// Wait for the block production loop to exit before closing resources.
<-startDone
if err := vc.Close(); err != nil {
log.Warnw("error closing vocone", "err", err)
}
Comment thread
p4u marked this conversation as resolved.
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module go.vocdoni.io/dvote

go 1.24.0
go 1.26.1

// For testing purposes
// replace go.vocdoni.io/proto => ../dvote-protobuf
Expand Down
2 changes: 1 addition & 1 deletion vochain/cometbft.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func (app *BaseApplication) InitChain(_ context.Context,
for k, v := range genesisAppState.TxCost.AsMap() {
err = app.State.SetTxBaseCost(k, v)
if err != nil {
return nil, fmt.Errorf("could not set tx cost %q to value %q from genesis file to the State", k, v)
return nil, fmt.Errorf("could not set tx cost %q to value %d from genesis file to the State", k, v)
}
}

Expand Down
229 changes: 229 additions & 0 deletions vocone/blockstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package vocone

import (
"encoding/binary"
"encoding/json"
"errors"
"time"

comettmhash "github.com/cometbft/cometbft/crypto/tmhash"
comettypes "github.com/cometbft/cometbft/types"
"go.vocdoni.io/dvote/crypto/ethereum"
"go.vocdoni.io/dvote/db"
"go.vocdoni.io/dvote/log"
"go.vocdoni.io/proto/build/go/models"
"google.golang.org/protobuf/proto"
)

// blockMeta holds persisted metadata for each block.
type blockMeta struct {
Timestamp int64 `json:"t"`
TxCount int32 `json:"n"`
StateRoot []byte `json:"r,omitempty"`
Hash []byte `json:"h,omitempty"`
ProposerAddress []byte `json:"p,omitempty"`
LastBlockHash []byte `json:"l,omitempty"`
DataHash []byte `json:"d,omitempty"`
}

// storeBlockMeta persists block metadata and a hash→height reverse index.
func (vc *Vocone) storeBlockMeta(height int64, timestamp time.Time, txCount int32,
stateRoot, blockHash, proposerAddr, lastBlockHash, dataHash []byte,
) error {
meta := blockMeta{
Timestamp: timestamp.UnixNano(),
TxCount: txCount,
StateRoot: stateRoot,
Hash: blockHash,
ProposerAddress: proposerAddr,
LastBlockHash: lastBlockHash,
DataHash: dataHash,
}
data, err := json.Marshal(&meta)
if err != nil {
return err
}
wTx := vc.blockStore.WriteTx()
defer wTx.Discard()
if prevData, err := vc.blockStore.Get(metaKey(height)); err == nil {
var prev blockMeta
if jsonErr := json.Unmarshal(prevData, &prev); jsonErr == nil && len(prev.Hash) > 0 {
if len(blockHash) == 0 || string(prev.Hash) != string(blockHash) {
if err := wTx.Delete(blockHashKey(prev.Hash)); err != nil {
return err
}
}
}
} else if !errors.Is(err, db.ErrKeyNotFound) {
return err
}
if err := wTx.Set(metaKey(height), data); err != nil {
return err
}
// Store hash→height reverse index for GetBlockByHash lookups.
if len(blockHash) > 0 {
heightBytes := make([]byte, 8)
binary.BigEndian.PutUint64(heightBytes, uint64(height))
if err := wTx.Set(blockHashKey(blockHash), heightBytes); err != nil {
return err
}
}
Comment on lines +46 to +70

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

storeBlockMeta writes/overwrites metaKey(height) and also writes a hash→height reverse index. If storeBlockMeta is called again for the same height (e.g., crash after writing meta but before CommitState, then re-execution with a different timestamp/hash), the old blockhash/ entry is never deleted, leaving stale hash→height mappings and causing GetBlockByHash to return an unrelated block. When overwriting existing metadata, delete the prior reverse-index key for the previous hash if it differs.

Copilot uses AI. Check for mistakes.
return wTx.Commit()
}

// loadBlockMeta reads block metadata from the store.
func (vc *Vocone) loadBlockMeta(height int64) (*blockMeta, error) {
data, err := vc.blockStore.Get(metaKey(height))
if err != nil {
return nil, err
}
var meta blockMeta
if err := json.Unmarshal(data, &meta); err != nil {
return nil, err
}
return &meta, nil
}

// buildBlock constructs a comettypes.Block with all header fields populated
// so that Block.Hash() returns a deterministic, non-nil hash.
func (vc *Vocone) buildBlock(height int64, timestamp time.Time, txs [][]byte, appHash []byte) *comettypes.Block {
blk := &comettypes.Block{
Header: comettypes.Header{
ChainID: vc.App.ChainID(),
Height: height,
Time: timestamp,
ProposerAddress: vc.proposerAddress,
AppHash: appHash,
// ValidatorsHash is required for Header.Hash() to return non-nil.
ValidatorsHash: comettmhash.Sum(vc.proposerAddress),
NextValidatorsHash: comettmhash.Sum(vc.proposerAddress),
ConsensusHash: comettmhash.Sum([]byte("vocone")),
},
// LastCommit must be non-nil for Block.Hash() to return non-nil.
LastCommit: &comettypes.Commit{Height: height - 1},
}
// Set the previous block hash if available.
if height > 0 {
if prevMeta, err := vc.loadBlockMeta(height - 1); err == nil && len(prevMeta.Hash) > 0 {
blk.Header.LastBlockID = comettypes.BlockID{Hash: prevMeta.Hash}
}
}
// Populate transactions.
blk.Data.Txs = make([]comettypes.Tx, len(txs))
for i, tx := range txs {
blk.Data.Txs[i] = tx
}
blk.Header.DataHash = blk.Data.Hash()
return blk
}

// getBlock reconstructs a block from the persistent store.
func (vc *Vocone) getBlock(height int64) *comettypes.Block {
if vc.closed.Load() {
return &comettypes.Block{Header: comettypes.Header{Height: height}}
}
meta, err := vc.loadBlockMeta(height)
if err != nil {
// No metadata — return a minimal block shell.
return &comettypes.Block{
Header: comettypes.Header{
ChainID: vc.App.ChainID(),
Height: height,
},
}
}

blk := &comettypes.Block{
Header: comettypes.Header{
ChainID: vc.App.ChainID(),
Height: height,
Time: time.Unix(0, meta.Timestamp),
ProposerAddress: meta.ProposerAddress,
AppHash: meta.StateRoot,
DataHash: meta.DataHash,
// Required for Hash() to return non-nil.
ValidatorsHash: comettmhash.Sum(meta.ProposerAddress),
NextValidatorsHash: comettmhash.Sum(meta.ProposerAddress),
ConsensusHash: comettmhash.Sum([]byte("vocone")),
},
// LastCommit must be non-nil for Block.Hash() to return non-nil.
LastCommit: &comettypes.Commit{Height: height - 1},
}
if len(meta.LastBlockHash) > 0 {
blk.Header.LastBlockID = comettypes.BlockID{Hash: meta.LastBlockHash}
}

// Read exactly txCount transactions
for i := int32(0); i < meta.TxCount; i++ {
txData, err := vc.blockStore.Get(txKey(height, i))
if err != nil {
log.Warnw("missing tx in block store",
"height", height, "index", i, "err", err)
break
}
blk.Data.Txs = append(blk.Data.Txs, txData)
}
return blk
}

// getBlockByHash looks up a block by its hash using the reverse index.
func (vc *Vocone) getBlockByHash(hash []byte) *comettypes.Block {
if vc.closed.Load() {
return nil
}
heightBytes, err := vc.blockStore.Get(blockHashKey(hash))
if err != nil {
return nil
}
Comment thread
p4u marked this conversation as resolved.
if len(heightBytes) != 8 {
return nil
}
height := int64(binary.BigEndian.Uint64(heightBytes))
return vc.getBlock(height)
}

// getTx retrieves a single transaction from the block store.
func (vc *Vocone) getTx(height uint32, txIndex int32) (*models.SignedTx, error) {
txData, err := vc.blockStore.Get(txKey(int64(height), txIndex))
if err != nil {
return nil, err
}
stx := &models.SignedTx{}
return stx, proto.Unmarshal(txData, stx)
}

// getTxWithHash retrieves a transaction and its hash from the block store.
func (vc *Vocone) getTxWithHash(height uint32, txIndex int32) (*models.SignedTx, []byte, error) {
txData, err := vc.blockStore.Get(txKey(int64(height), txIndex))
if err != nil {
return nil, nil, err
}
stx := &models.SignedTx{}
return stx, ethereum.HashRaw(txData), proto.Unmarshal(txData, stx)
}

// txKey builds the db key for a transaction: "tx/" + height (8 bytes BE) + "/" + txIndex (4 bytes BE).
func txKey(height int64, txIndex int32) []byte {
key := make([]byte, len(prefixTx)+8+1+4)
copy(key, prefixTx)
binary.BigEndian.PutUint64(key[len(prefixTx):], uint64(height))
key[len(prefixTx)+8] = '/'
binary.BigEndian.PutUint32(key[len(prefixTx)+9:], uint32(txIndex))
return key
}

// metaKey builds the db key for block metadata: "meta/" + height (8 bytes BE).
func metaKey(height int64) []byte {
key := make([]byte, len(prefixMeta)+8)
copy(key, prefixMeta)
binary.BigEndian.PutUint64(key[len(prefixMeta):], uint64(height))
return key
}

// blockHashKey builds the db key for the hash→height reverse index: "blockhash/" + hash.
func blockHashKey(hash []byte) []byte {
key := make([]byte, len(prefixBlockHash)+len(hash))
copy(key, prefixBlockHash)
copy(key[len(prefixBlockHash):], hash)
return key
}
Loading
Loading