diff --git a/api/chain.go b/api/chain.go index c1f77205a..197e22b57 100644 --- a/api/chain.go +++ b/api/chain.go @@ -300,9 +300,17 @@ func (a *API) chainInfoHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) return err } + var blockTimesInMs [5]uint64 + for i, v := range a.vocinfo.BlockTimes() { + blockTimesInMs[i] = uint64(v.Milliseconds()) + } + if blockTimesInMs[0] == 0 { + blockTimesInMs[0] = uint64(a.vocapp.BlockTimeTarget().Milliseconds()) + } + data, err := json.Marshal(&ChainInfo{ ID: a.vocapp.ChainID(), - BlockTime: a.vocinfo.BlockTimes(), + BlockTime: blockTimesInMs, ElectionCount: a.indexer.CountTotalProcesses(), OrganizationCount: a.indexer.CountTotalEntities(), Height: a.vocapp.Height(), diff --git a/cmd/voconed/voconed.go b/cmd/voconed/voconed.go index f2d387cea..cc0b18a8b 100644 --- a/cmd/voconed/voconed.go +++ b/cmd/voconed/voconed.go @@ -223,7 +223,7 @@ func main() { } } - vc.SetBlockTimeTarget(time.Second * time.Duration(config.blockSeconds)) + vc.App.SetBlockTimeTarget(time.Second * time.Duration(config.blockSeconds)) vc.SetBlockSize(config.blockSize) go vc.Start() diff --git a/service/vochain.go b/service/vochain.go index c8ecc7b78..6ebcd236d 100644 --- a/service/vochain.go +++ b/service/vochain.go @@ -174,13 +174,13 @@ func VochainPrintInfo(interval time.Duration, vi *vochaininfo.VochainInfo) { b.Reset() a := vi.BlockTimes() if a[1] > 0 { - fmt.Fprintf(&b, "10m:%.2f", float32(a[1])/1000) + fmt.Fprintf(&b, "10m:%s", a[1].Truncate(time.Millisecond)) } if a[2] > 0 { - fmt.Fprintf(&b, " 1h:%.2f", float32(a[2])/1000) + fmt.Fprintf(&b, " 1h:%s", a[2].Truncate(time.Millisecond)) } if a[4] > 0 { - fmt.Fprintf(&b, " 24h:%.2f", float32(a[4])/1000) + fmt.Fprintf(&b, " 24h:%s", a[4].Truncate(time.Millisecond)) } h = vi.Height() m = vi.MempoolSize() diff --git a/types/consts.go b/types/consts.go index f6b9ce29e..9c09c3c3a 100644 --- a/types/consts.go +++ b/types/consts.go @@ -36,7 +36,7 @@ const ( ArchiveURL = "/ipns/k2k4r8otxrf176h1i08txap0ep6ynr1jac0vymozi068eedml7gk1595" // DefaultBlockTime is the default block time in seconds. - DefaultBlockTime = 12 * time.Second + DefaultBlockTime = 10 * time.Second // KeyKeeperMaxKeyIndex is the maxim number of allowed encryption keys. KeyKeeperMaxKeyIndex = 16 diff --git a/vochain/app.go b/vochain/app.go index 8e2c341b2..80af49a7b 100644 --- a/vochain/app.go +++ b/vochain/app.go @@ -18,6 +18,7 @@ import ( "go.vocdoni.io/dvote/config" "go.vocdoni.io/dvote/crypto/zk/circuit" "go.vocdoni.io/dvote/test/testcommon/testutil" + "go.vocdoni.io/dvote/types" "go.vocdoni.io/dvote/vochain/ist" vstate "go.vocdoni.io/dvote/vochain/state" "go.vocdoni.io/dvote/vochain/transaction" @@ -72,6 +73,9 @@ type BaseApplication struct { // was seen frist time and the number of attempts failed for including it into a block. txReferences sync.Map + // blockTime is the target block time that miners use + blockTime time.Duration + // endBlockTimestamp is the last block end timestamp calculated from local time. endBlockTimestamp atomic.Int64 // startBlockTimestamp is the current block timestamp from tendermint's @@ -413,3 +417,16 @@ func (app *BaseApplication) TimestampFromBlock(height int64) *time.Time { func (app *BaseApplication) MempoolSize() int { return app.fnMempoolSize() } + +// BlockTimeTarget returns the current block time target +func (app *BaseApplication) BlockTimeTarget() time.Duration { + if app.blockTime == 0 { + return types.DefaultBlockTime + } + return app.blockTime +} + +// SetBlockTimeTarget sets the current block time target +func (app *BaseApplication) SetBlockTimeTarget(d time.Duration) { + app.blockTime = d +} diff --git a/vochain/vochaininfo/vochaininfo.go b/vochain/vochaininfo/vochaininfo.go index a7ef785aa..8c227b2d3 100644 --- a/vochain/vochaininfo/vochaininfo.go +++ b/vochain/vochaininfo/vochaininfo.go @@ -3,14 +3,12 @@ package vochaininfo import ( "context" "fmt" - "math" "sync" "time" "github.com/VictoriaMetrics/metrics" coretypes "github.com/cometbft/cometbft/rpc/core/types" "go.vocdoni.io/dvote/log" - "go.vocdoni.io/dvote/types" "go.vocdoni.io/dvote/vochain" "go.vocdoni.io/dvote/vochain/state" ) @@ -19,7 +17,7 @@ import ( // Avg1/10/60/360 are the block time average for 1 minute, 10 minutes, 1 hour and 6 hours type VochainInfo struct { votesPerMinute uint64 - blockTimes [5]uint64 + blockTimes [5]time.Duration blocksMinute float64 vnode *vochain.BaseApplication close chan bool @@ -63,7 +61,7 @@ func (vi *VochainInfo) updateCounters() { voteCacheSize.Set(uint64(vi.vnode.State.CacheSize())) mempoolSize.Set(uint64(vi.vnode.MempoolSize())) - blockPeriodMinute.Set(vi.BlockTimes()[0]) + blockPeriodMinute.Set(uint64(vi.BlockTimes()[0].Milliseconds())) blocksSyncLastMinute.Set(uint64(vi.BlocksLastMinute())) } @@ -74,7 +72,7 @@ func (vi *VochainInfo) Height() uint64 { // averageBlockTime calculates the average block time for the last intervalBlocks blocks. // The timestamp information is taken from the block headers. -func (vi *VochainInfo) averageBlockTime(intervalBlocks int64) float64 { +func (vi *VochainInfo) averageBlockTime(intervalBlocks int64) time.Duration { if intervalBlocks == 0 { return 0 } @@ -91,81 +89,69 @@ func (vi *VochainInfo) averageBlockTime(intervalBlocks int64) float64 { startTime := vi.vnode.TimestampFromBlock(startBlockHeight) currentTime := vi.vnode.TimestampFromBlock(currentHeight) - if startTime == nil || currentTime == nil { + if startTime == nil || currentTime == nil || startTime.Equal(time.Time{}) { return 0 } // Calculate the time frame in seconds - timeFrameSeconds := currentTime.Sub(*startTime).Seconds() + timeFrame := currentTime.Sub(*startTime) // Adjust the average block time based on the actual time frame - return timeFrameSeconds / float64(intervalBlocks) + return timeFrame / time.Duration(intervalBlocks) } func (vi *VochainInfo) updateBlockTimes() { - vi.blockTimes = [5]uint64{ - uint64(1000 * vi.averageBlockTime(60/int64(types.DefaultBlockTime.Seconds())*1)), - uint64(1000 * vi.averageBlockTime(60/int64(types.DefaultBlockTime.Seconds())*10)), - uint64(1000 * vi.averageBlockTime(60/int64(types.DefaultBlockTime.Seconds())*60)), - uint64(1000 * vi.averageBlockTime(60/int64(types.DefaultBlockTime.Seconds())*360)), - uint64(1000 * vi.averageBlockTime(60/int64(types.DefaultBlockTime.Seconds())*1440)), + vi.blockTimes = [5]time.Duration{ + vi.averageBlockTime(int64(1 * time.Minute / vi.vnode.BlockTimeTarget())), + vi.averageBlockTime(int64(10 * time.Minute / vi.vnode.BlockTimeTarget())), + vi.averageBlockTime(int64(1 * time.Hour / vi.vnode.BlockTimeTarget())), + vi.averageBlockTime(int64(6 * time.Hour / vi.vnode.BlockTimeTarget())), + vi.averageBlockTime(int64(24 * time.Hour / vi.vnode.BlockTimeTarget())), + } + if vi.blockTimes[0] == 0 { + vi.blockTimes[0] = vi.vnode.BlockTimeTarget() } } -// BlockTimes returns the average block time in milliseconds for 1, 10, 60, 360 and 1440 minutes. +// BlockTimes returns the average block time for 1, 10, 60, 360 and 1440 minutes. // Value 0 means there is not yet an average. -func (vi *VochainInfo) BlockTimes() [5]uint64 { +func (vi *VochainInfo) BlockTimes() [5]time.Duration { vi.lock.RLock() defer vi.lock.RUnlock() return vi.blockTimes } -// EstimateBlockHeight provides an estimation time for a future blockchain height number. -func (vi *VochainInfo) EstimateBlockHeight(target time.Time) (uint64, error) { - currentTime := time.Now() - // diff time in seconds - diffTime := target.Unix() - currentTime.Unix() - - // block time in ms +func (vi *VochainInfo) getBlockTimeBestEstimate(i int) time.Duration { times := vi.BlockTimes() - getMaxTimeFrom := func(i int) uint64 { - for ; i >= 0; i-- { - if times[i] != 0 { - return uint64(times[i]) - } + for ; i >= 0; i-- { + if times[i] != 0 { + return times[i] } - return 10000 // fallback } - inPast := diffTime < 0 - absDiff := diffTime - // check diff is not too big - if absDiff > math.MaxUint64/1000 { - return 0, fmt.Errorf("target time %v is too far in the future", target) - } - if inPast { - absDiff = -absDiff - } - t := uint64(0) - switch { - // if less than around 15 minutes missing - case absDiff < 900: - t = getMaxTimeFrom(1) - // if less than around 6 hours missing - case absDiff < 21600: - t = getMaxTimeFrom(3) - // if more than around 6 hours missing - default: - t = getMaxTimeFrom(4) - } - // Multiply by 1000 because t is represented in seconds, not ms. - // Dividing t first can floor the integer, leading to divide-by-zero - currentHeight := uint64(vi.Height()) - blockDiff := uint64(absDiff*1000) / t - if inPast { - if blockDiff > currentHeight { - return 0, fmt.Errorf("target time %v is before origin", target) + return vi.vnode.BlockTimeTarget() // fallback +} + +// EstimateBlockHeight provides an estimated blockchain height for a future or past date. +func (vi *VochainInfo) EstimateBlockHeight(target time.Time) (uint64, error) { + timeDiff := time.Until(target) + timeBetweenBlocks := func() time.Duration { + switch { + // if less than around 15 minutes missing + case timeDiff.Abs().Minutes() < 15: + return vi.getBlockTimeBestEstimate(1) + // if less than around 6 hours missing + case timeDiff.Abs().Hours() < 6: + return vi.getBlockTimeBestEstimate(3) + // if more than around 6 hours missing + default: + return vi.getBlockTimeBestEstimate(4) } - return currentHeight - blockDiff, nil + }() + blockDiff := uint64(timeDiff / timeBetweenBlocks) + currentHeight := uint64(vi.vnode.Height()) + // timeDiff is negative if target is in the past + if timeDiff < 0 && currentHeight+blockDiff <= 0 { + return 0, fmt.Errorf("target time %v is before genesis", target) } return currentHeight + blockDiff, nil } @@ -173,11 +159,9 @@ func (vi *VochainInfo) EstimateBlockHeight(target time.Time) (uint64, error) { // HeightTime estimates the UTC time for a future height or returns the // block timestamp if height is in the past. func (vi *VochainInfo) HeightTime(height uint64) time.Time { - times := vi.BlockTimes() - currentHeight := vi.Height() - diffHeight := int64(height - currentHeight) + diffHeight := int64(height - uint64(vi.vnode.Height())) - if diffHeight < 0 { + if diffHeight < 0 { // height is in the past blk := vi.vnode.GetBlockByHeight(int64(height)) if blk == nil { log.Errorf("cannot get block height %d", height) @@ -186,28 +170,20 @@ func (vi *VochainInfo) HeightTime(height uint64) time.Time { return blk.Header.Time } - getMaxTimeFrom := func(i int) uint64 { - for ; i >= 0; i-- { - if times[i] != 0 { - return times[i] - } + timeBetweenBlocks := func() time.Duration { + switch { + // if less than around 15 minutes missing + case diffHeight < int64(15*time.Minute/vi.vnode.BlockTimeTarget()): + return vi.getBlockTimeBestEstimate(1) + // if less than around 6 hours missing + case diffHeight < int64(6*time.Hour/vi.vnode.BlockTimeTarget()): + return vi.getBlockTimeBestEstimate(3) + // if more than around 6 hours missing + default: + return vi.getBlockTimeBestEstimate(4) } - return 10000 // fallback - } - - t := uint64(0) - switch { - // if less than around 15 minutes missing - case diffHeight < 100: - t = getMaxTimeFrom(1) - // if less than around 6 hours missing - case diffHeight < 1000: - t = getMaxTimeFrom(3) - // if less than around 6 hours missing - case diffHeight >= 1000: - t = getMaxTimeFrom(4) - } - return time.Now().Add(time.Duration(diffHeight*int64(t)) * time.Millisecond) + }() + return time.Now().Add(time.Duration(diffHeight) * timeBetweenBlocks) } // TreeSizes returns the current size of the ProcessTree, VoteTree and the votes per minute diff --git a/vocone/vocone.go b/vocone/vocone.go index ffd7a5235..31110297a 100644 --- a/vocone/vocone.go +++ b/vocone/vocone.go @@ -50,8 +50,8 @@ type Vocone struct { mempool chan []byte // a buffered channel acts like a FIFO with a fixed size blockStore db.Database height atomic.Int64 + blockTimestamps sync.Map lastBlockTime time.Time - blockTimeTarget time.Duration txsPerBlock int // vcMtx is a lock on modification to the app state. // this enables direct calls to vochain functions from the vocone @@ -72,7 +72,6 @@ func NewVocone(dataDir string, keymanager *ethereum.SignKeys, disableIPFS bool, return nil, err } vc.mempool = make(chan []byte, mempoolSize) - vc.blockTimeTarget = DefaultBlockTimeTarget vc.txsPerBlock = DefaultTxsPerBlock version, err := vc.App.State.LastHeight() if err != nil { @@ -86,6 +85,7 @@ func NewVocone(dataDir string, keymanager *ethereum.SignKeys, disableIPFS bool, vc.setDefaultMethods() vc.App.State.SetHeight(uint32(vc.height.Load())) + vc.App.SetBlockTimeTarget(DefaultBlockTimeTarget) // Create indexer if vc.Indexer, err = indexer.New(vc.App, indexer.Options{ @@ -200,19 +200,14 @@ func (vc *Vocone) Start() { // Waiting time sinceLast := time.Since(vc.lastBlockTime) - if sinceLast < vc.blockTimeTarget { - time.Sleep(vc.blockTimeTarget - sinceLast) + if sinceLast < vc.App.BlockTimeTarget() { + time.Sleep(vc.App.BlockTimeTarget() - sinceLast) } vc.lastBlockTime = time.Now() vc.height.Add(1) } } -// SetBlockTimeTarget configures the time window in which blocks will be created. -func (vc *Vocone) SetBlockTimeTarget(targetTime time.Duration) { - vc.blockTimeTarget = targetTime -} - // SetBlockSize configures the maximum number of transactions per block. func (vc *Vocone) SetBlockSize(txsCount int) { vc.txsPerBlock = txsCount @@ -368,6 +363,7 @@ func (vc *Vocone) addTx(tx []byte) (*tmcoretypes.ResultBroadcastTx, error) { // prepareBlock prepares a block with transactions from the mempool and returns the list of transactions. func (vc *Vocone) prepareBlock() [][]byte { + defer vc.blockTimestamps.Store(vc.height.Load(), time.Now()) blockStoreTx := vc.blockStore.WriteTx() defer blockStoreTx.Discard() var transactions [][]byte @@ -407,6 +403,14 @@ txLoop: // TO-DO: improve this function func (vc *Vocone) getBlock(height int64) *tmtypes.Block { blk := new(tmtypes.Block) + blk.Height = height + blk.ChainID = vc.App.ChainID() + v, found := vc.blockTimestamps.Load(height) + if found { + if t, ok := v.(time.Time); ok { + blk.Time = t + } + } for i := int32(0); ; i++ { tx, err := vc.getTx(uint32(height), i) if err != nil { @@ -454,19 +458,19 @@ func vochainPrintInfo(interval time.Duration, vi *vochaininfo.VochainInfo) { b.Reset() a := vi.BlockTimes() if a[0] > 0 { - fmt.Fprintf(&b, "1m:%.2f", float32(a[0])/1000) + fmt.Fprintf(&b, "1m:%s", a[0].Truncate(time.Millisecond)) } if a[1] > 0 { - fmt.Fprintf(&b, " 10m:%.2f", float32(a[1])/1000) + fmt.Fprintf(&b, " 10m:%s", a[1].Truncate(time.Millisecond)) } if a[2] > 0 { - fmt.Fprintf(&b, " 1h:%.2f", float32(a[2])/1000) + fmt.Fprintf(&b, " 1h:%s", a[2].Truncate(time.Millisecond)) } if a[3] > 0 { - fmt.Fprintf(&b, " 6h:%.2f", float32(a[3])/1000) + fmt.Fprintf(&b, " 6h:%s", a[3].Truncate(time.Millisecond)) } if a[4] > 0 { - fmt.Fprintf(&b, " 24h:%.2f", float32(a[4])/1000) + fmt.Fprintf(&b, " 24h:%s", a[4].Truncate(time.Millisecond)) } h = vi.Height() m = vi.MempoolSize() diff --git a/vocone/vocone_test.go b/vocone/vocone_test.go index 420c48dcf..419200444 100644 --- a/vocone/vocone_test.go +++ b/vocone/vocone_test.go @@ -34,7 +34,7 @@ func TestVocone(t *testing.T) { vc, err := NewVocone(dir, &keymng, false, "", nil) qt.Assert(t, err, qt.IsNil) - vc.SetBlockTimeTarget(time.Millisecond * 500) + vc.App.SetBlockTimeTarget(time.Millisecond * 500) go vc.Start() port := 13000 + util.RandomInt(0, 2000) _, err = vc.EnableAPI("127.0.0.1", port, "/v2")