mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +00:00

* Fix stability test mining (allow submission of non DAA blocks too) * Comments for go fmt * Use SubmitBlockAlsoIfNonDAA for all tests Co-authored-by: Elichai Turkel <elichai.turkel@gmail.com>
320 lines
11 KiB
Go
320 lines
11 KiB
Go
package daa
|
|
|
|
import (
|
|
"github.com/kaspanet/kaspad/app/appmessage"
|
|
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
|
"github.com/kaspanet/kaspad/domain/consensus/utils/pow"
|
|
"github.com/kaspanet/kaspad/domain/dagconfig"
|
|
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
|
|
"github.com/kaspanet/kaspad/stability-tests/common"
|
|
"math"
|
|
"math/rand"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const rpcAddress = "localhost:9000"
|
|
const miningAddress = "kaspadev:qrcqat6l9zcjsu7swnaztqzrv0s7hu04skpaezxk43y4etj8ncwfkuhy0zmax"
|
|
const blockRateDeviationThreshold = 0.5
|
|
const averageBlockRateSampleSize = 60
|
|
const averageHashRateSampleSize = 100_000
|
|
|
|
func TestDAA(t *testing.T) {
|
|
if os.Getenv("RUN_STABILITY_TESTS") == "" {
|
|
t.Skip()
|
|
}
|
|
|
|
machineHashNanoseconds := measureMachineHashNanoseconds(t)
|
|
t.Logf("Machine hashes per second: %d", hashNanosecondsToHashesPerSecond(machineHashNanoseconds))
|
|
|
|
tests := []struct {
|
|
name string
|
|
runDuration time.Duration
|
|
|
|
// targetHashNanosecondsFunction receives the duration of time between now and the start
|
|
// of the run (moments before the first hash has been calculated). It returns the target
|
|
// duration of a single hash operation in nanoseconds (greater return value = lower hash rate)
|
|
targetHashNanosecondsFunction func(totalElapsedDuration time.Duration) int64
|
|
}{
|
|
{
|
|
name: "constant hash rate",
|
|
runDuration: 10 * time.Minute,
|
|
targetHashNanosecondsFunction: func(totalElapsedDuration time.Duration) int64 {
|
|
return machineHashNanoseconds * 2
|
|
},
|
|
},
|
|
{
|
|
name: "sudden hash rate drop",
|
|
runDuration: 45 * time.Minute,
|
|
targetHashNanosecondsFunction: func(totalElapsedDuration time.Duration) int64 {
|
|
if totalElapsedDuration < 5*time.Minute {
|
|
return machineHashNanoseconds * 2
|
|
}
|
|
return machineHashNanoseconds * 10
|
|
},
|
|
},
|
|
{
|
|
name: "sudden hash rate jump",
|
|
runDuration: 15 * time.Minute,
|
|
targetHashNanosecondsFunction: func(totalElapsedDuration time.Duration) int64 {
|
|
if totalElapsedDuration < 5*time.Minute {
|
|
return machineHashNanoseconds * 10
|
|
}
|
|
return machineHashNanoseconds * 2
|
|
},
|
|
},
|
|
{
|
|
name: "hash rate peak",
|
|
runDuration: 10 * time.Minute,
|
|
targetHashNanosecondsFunction: func(totalElapsedDuration time.Duration) int64 {
|
|
if totalElapsedDuration > 4*time.Minute && totalElapsedDuration < 5*time.Minute {
|
|
return machineHashNanoseconds * 2
|
|
}
|
|
return machineHashNanoseconds * 10
|
|
},
|
|
},
|
|
{
|
|
name: "hash rate valley",
|
|
runDuration: 10 * time.Minute,
|
|
targetHashNanosecondsFunction: func(totalElapsedDuration time.Duration) int64 {
|
|
if totalElapsedDuration > 4*time.Minute && totalElapsedDuration < 5*time.Minute {
|
|
return machineHashNanoseconds * 10
|
|
}
|
|
return machineHashNanoseconds * 2
|
|
},
|
|
},
|
|
{
|
|
name: "periodic hash rate peaks",
|
|
runDuration: 10 * time.Minute,
|
|
targetHashNanosecondsFunction: func(totalElapsedDuration time.Duration) int64 {
|
|
if int(totalElapsedDuration.Seconds())%30 == 0 {
|
|
return machineHashNanoseconds * 2
|
|
}
|
|
return machineHashNanoseconds * 10
|
|
},
|
|
},
|
|
{
|
|
name: "periodic hash rate valleys",
|
|
runDuration: 10 * time.Minute,
|
|
targetHashNanosecondsFunction: func(totalElapsedDuration time.Duration) int64 {
|
|
if int(totalElapsedDuration.Seconds())%30 == 0 {
|
|
return machineHashNanoseconds * 10
|
|
}
|
|
return machineHashNanoseconds * 2
|
|
},
|
|
},
|
|
{
|
|
name: "constant exponential hash rate increase",
|
|
runDuration: 15 * time.Minute,
|
|
targetHashNanosecondsFunction: func(totalElapsedDuration time.Duration) int64 {
|
|
fromHashNanoseconds := machineHashNanoseconds * 10
|
|
toHashNanoseconds := machineHashNanoseconds * 2
|
|
|
|
if totalElapsedDuration < 10*time.Minute {
|
|
exponentialIncreaseDuration := 10 * time.Minute
|
|
timeElapsedFraction := float64(totalElapsedDuration.Nanoseconds()) / float64(exponentialIncreaseDuration.Nanoseconds())
|
|
|
|
return fromHashNanoseconds -
|
|
int64(math.Pow(float64(fromHashNanoseconds-toHashNanoseconds), timeElapsedFraction))
|
|
}
|
|
|
|
// 5 minute cooldown. We expect the DAA to still be "catching up" at the end
|
|
// of the exponential increase so, for the sake of testing, we wait a while for
|
|
// the hash rate to stabilize
|
|
|
|
return toHashNanoseconds
|
|
},
|
|
},
|
|
{
|
|
name: "constant exponential hash rate decrease",
|
|
runDuration: 45 * time.Minute,
|
|
targetHashNanosecondsFunction: func(totalElapsedDuration time.Duration) int64 {
|
|
fromHashNanoseconds := machineHashNanoseconds * 2
|
|
toHashNanoseconds := machineHashNanoseconds * 10
|
|
|
|
if totalElapsedDuration < 10*time.Minute {
|
|
exponentialDecreaseDuration := 10 * time.Minute
|
|
timeElapsedFraction := float64(totalElapsedDuration.Nanoseconds()) / float64(exponentialDecreaseDuration.Nanoseconds())
|
|
|
|
return fromHashNanoseconds +
|
|
int64(math.Pow(float64(toHashNanoseconds-fromHashNanoseconds), timeElapsedFraction))
|
|
}
|
|
|
|
// 5 minute cooldown. We expect the DAA to still be "catching up" at the end
|
|
// of the exponential decrease so, for the sake of testing, we wait a while for
|
|
// the hash rate to stabilize
|
|
|
|
return toHashNanoseconds
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
runDAATest(t, test.name, test.runDuration, test.targetHashNanosecondsFunction)
|
|
}
|
|
}
|
|
|
|
func measureMachineHashNanoseconds(t *testing.T) int64 {
|
|
t.Logf("Measuring machine hash rate")
|
|
defer t.Logf("Finished measuring machine hash rate")
|
|
|
|
genesisBlock := dagconfig.DevnetParams.GenesisBlock
|
|
state := pow.NewState(genesisBlock.Header.ToMutable())
|
|
|
|
machineHashesPerSecondMeasurementDuration := 10 * time.Second
|
|
hashes := int64(0)
|
|
state.Nonce = rand.Uint64()
|
|
loopForDuration(machineHashesPerSecondMeasurementDuration, func(isFinished *bool) {
|
|
state.CheckProofOfWork()
|
|
hashes++
|
|
state.IncrementNonce()
|
|
})
|
|
|
|
return machineHashesPerSecondMeasurementDuration.Nanoseconds() / hashes
|
|
}
|
|
|
|
func runDAATest(t *testing.T, testName string, runDuration time.Duration,
|
|
targetHashNanosecondsFunction func(totalElapsedDuration time.Duration) int64) {
|
|
|
|
t.Logf("DAA TEST STARTED: %s", testName)
|
|
defer t.Logf("DAA TEST FINISHED: %s", testName)
|
|
|
|
tearDownKaspad := common.RunKaspadForTesting(t, "kaspad-daa-test", rpcAddress)
|
|
defer tearDownKaspad()
|
|
|
|
rpcClient, err := rpcclient.NewRPCClient(rpcAddress)
|
|
if err != nil {
|
|
t.Fatalf("NewRPCClient: %s", err)
|
|
}
|
|
|
|
// These variables are for gathering stats. Useful mostly for debugging
|
|
averageHashDuration := newAverageDuration(averageHashRateSampleSize)
|
|
averageMiningDuration := newAverageDuration(averageBlockRateSampleSize)
|
|
previousDifficulty := float64(0)
|
|
blocksMined := 0
|
|
|
|
// Mine blocks the same way a CPU miner mines blocks until `runDuration` elapses
|
|
startTime := time.Now()
|
|
loopForDuration(runDuration, func(isFinished *bool) {
|
|
templateBlock := fetchBlockForMining(t, rpcClient)
|
|
headerForMining := templateBlock.Header.ToMutable()
|
|
minerState := pow.NewState(headerForMining)
|
|
|
|
// Try hashes until we find a valid block
|
|
miningStartTime := time.Now()
|
|
minerState.Nonce = rand.Uint64()
|
|
for {
|
|
hashStartTime := time.Now()
|
|
|
|
if minerState.CheckProofOfWork() {
|
|
headerForMining.SetNonce(minerState.Nonce)
|
|
templateBlock.Header = headerForMining.ToImmutable()
|
|
break
|
|
}
|
|
|
|
// Throttle the hash rate by waiting until the target hash duration elapses
|
|
waitUntilTargetHashDurationHadElapsed(startTime, hashStartTime, targetHashNanosecondsFunction)
|
|
|
|
// Collect stats about hash rate
|
|
hashDuration := time.Since(hashStartTime)
|
|
averageHashDuration.add(hashDuration)
|
|
|
|
// Exit early if the test is finished
|
|
if *isFinished {
|
|
return
|
|
}
|
|
minerState.IncrementNonce()
|
|
}
|
|
|
|
// Collect stats about block rate
|
|
miningDuration := time.Since(miningStartTime)
|
|
averageMiningDuration.add(miningDuration)
|
|
|
|
logMinedBlockStatsAndUpdateStatFields(t, rpcClient, averageMiningDuration, averageHashDuration, startTime,
|
|
miningDuration, &previousDifficulty, &blocksMined)
|
|
|
|
// Exit early if the test is finished
|
|
if *isFinished {
|
|
return
|
|
}
|
|
|
|
submitMinedBlock(t, rpcClient, templateBlock)
|
|
})
|
|
|
|
averageMiningDurationInSeconds := averageMiningDuration.toDuration().Seconds()
|
|
expectedAverageMiningDurationInSeconds := float64(1)
|
|
deviation := math.Abs(expectedAverageMiningDurationInSeconds - averageMiningDurationInSeconds)
|
|
if deviation > blockRateDeviationThreshold {
|
|
t.Errorf("Block rate deviation %f is higher than threshold %f. Want: %f, got: %f",
|
|
deviation, blockRateDeviationThreshold, expectedAverageMiningDurationInSeconds, averageMiningDurationInSeconds)
|
|
}
|
|
}
|
|
|
|
func fetchBlockForMining(t *testing.T, rpcClient *rpcclient.RPCClient) *externalapi.DomainBlock {
|
|
getBlockTemplateResponse, err := rpcClient.GetBlockTemplate(miningAddress)
|
|
if err != nil {
|
|
t.Fatalf("GetBlockTemplate: %s", err)
|
|
}
|
|
templateBlock, err := appmessage.RPCBlockToDomainBlock(getBlockTemplateResponse.Block)
|
|
if err != nil {
|
|
t.Fatalf("RPCBlockToDomainBlock: %s", err)
|
|
}
|
|
return templateBlock
|
|
}
|
|
|
|
func waitUntilTargetHashDurationHadElapsed(startTime time.Time, hashStartTime time.Time,
|
|
targetHashNanosecondsFunction func(totalElapsedDuration time.Duration) int64) {
|
|
|
|
// Yielding a thread in Go takes up to a few milliseconds whereas hashing once
|
|
// takes a few hundred nanoseconds, so we spin in place instead of e.g. calling time.Sleep()
|
|
for {
|
|
targetHashNanoseconds := targetHashNanosecondsFunction(time.Since(startTime))
|
|
hashElapsedDurationNanoseconds := time.Since(hashStartTime).Nanoseconds()
|
|
if hashElapsedDurationNanoseconds >= targetHashNanoseconds {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func logMinedBlockStatsAndUpdateStatFields(t *testing.T, rpcClient *rpcclient.RPCClient,
|
|
averageMiningDuration *averageDuration, averageHashDurations *averageDuration,
|
|
startTime time.Time, miningDuration time.Duration, previousDifficulty *float64, blocksMined *int) {
|
|
|
|
averageMiningDurationAsDuration := averageMiningDuration.toDuration()
|
|
averageHashNanoseconds := averageHashDurations.toDuration().Nanoseconds()
|
|
averageHashesPerSecond := hashNanosecondsToHashesPerSecond(averageHashNanoseconds)
|
|
blockDAGInfoResponse, err := rpcClient.GetBlockDAGInfo()
|
|
if err != nil {
|
|
t.Fatalf("GetBlockDAGInfo: %s", err)
|
|
}
|
|
difficultyDelta := blockDAGInfoResponse.Difficulty - *previousDifficulty
|
|
*previousDifficulty = blockDAGInfoResponse.Difficulty
|
|
*blocksMined++
|
|
t.Logf("Mined block. Took: %s, average block mining duration: %s, "+
|
|
"average hashes per second: %d, difficulty delta: %f, time elapsed: %s, blocks mined: %d",
|
|
miningDuration, averageMiningDurationAsDuration, averageHashesPerSecond, difficultyDelta, time.Since(startTime), *blocksMined)
|
|
}
|
|
|
|
func submitMinedBlock(t *testing.T, rpcClient *rpcclient.RPCClient, block *externalapi.DomainBlock) {
|
|
_, err := rpcClient.SubmitBlockAlsoIfNonDAA(block)
|
|
if err != nil {
|
|
t.Fatalf("SubmitBlock: %s", err)
|
|
}
|
|
}
|
|
|
|
func hashNanosecondsToHashesPerSecond(hashNanoseconds int64) int64 {
|
|
return time.Second.Nanoseconds() / hashNanoseconds
|
|
}
|
|
|
|
func loopForDuration(duration time.Duration, runFunction func(isFinished *bool)) {
|
|
isFinished := false
|
|
go func() {
|
|
for !isFinished {
|
|
runFunction(&isFinished)
|
|
}
|
|
}()
|
|
time.Sleep(duration)
|
|
isFinished = true
|
|
}
|