diff --git a/.github/workflows/go-race.yml b/.github/workflows/go-race.yml new file mode 100644 index 000000000..50340d35b --- /dev/null +++ b/.github/workflows/go-race.yml @@ -0,0 +1,49 @@ +name: Go-Race + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + race_test: + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + branch: [ master, latest ] + name: Race detection on ${{ matrix.branch }} + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: 1.15 + + - name: Set scheduled branch name + shell: bash + if: github.event_name == 'schedule' + run: | + if [ "${{ matrix.branch }}" == "master" ]; then + echo "run_on=master" >> $GITHUB_ENV + fi + if [ "${{ matrix.branch }}" == "latest" ]; then + branch=$(git branch -r | grep 'v\([0-9]\+\.\)\([0-9]\+\.\)\([0-9]\+\)-dev' | sort -Vr | head -1 | xargs) + echo "run_on=${branch}" >> $GITHUB_ENV + fi + + - name: Set manual branch name + shell: bash + if: github.event_name == 'workflow_dispatch' + run: echo "run_on=${{ github.ref }}" >> $GITHUB_ENV + + - name: Test with race detector + shell: bash + run: | + git checkout "${{ env.run_on }}" + git status + go test -race ./... diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9ac54e3ba..8e646837f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,6 +11,7 @@ jobs: build: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ ubuntu-16.04, macos-10.15 ] name: Testing on on ${{ matrix.os }} @@ -34,7 +35,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v2 with: - go-version: 1.15 + go-version: 1.16 # Source: https://github.com/actions/cache/blob/main/examples.md#go---modules @@ -48,7 +49,7 @@ jobs: - name: Test shell: bash - run: ./build_and_test.sh + run: ./build_and_test.sh -v coverage: runs-on: ubuntu-20.04 @@ -60,10 +61,10 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v2 with: - go-version: 1.15 + go-version: 1.16 - name: Create coverage file - run: go test -covermode=atomic -coverpkg=./... -coverprofile coverage.txt ./... + run: go test -v -covermode=atomic -coverpkg=./... -coverprofile coverage.txt ./... - name: Upload coverage file run: bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/README.md b/README.md index be2213baf..fa5628220 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Kaspa is an attempt at a proof-of-work cryptocurrency with instant confirmations ## Requirements -Go 1.15 or later. +Go 1.16 or later. ## Installation diff --git a/app/protocol/flowcontext/blocks.go b/app/protocol/flowcontext/blocks.go index 6eacd7dd2..3ddad114e 100644 --- a/app/protocol/flowcontext/blocks.go +++ b/app/protocol/flowcontext/blocks.go @@ -2,6 +2,7 @@ package flowcontext import ( peerpkg "github.com/kaspanet/kaspad/app/protocol/peer" + "github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/pkg/errors" @@ -98,6 +99,10 @@ func (f *FlowContext) SharedRequestedBlocks() *blockrelay.SharedRequestedBlocks // AddBlock adds the given block to the DAG and propagates it. func (f *FlowContext) AddBlock(block *externalapi.DomainBlock) error { + if len(block.Transactions) == 0 { + return protocolerrors.Errorf(false, "cannot add header only block") + } + blockInsertionResult, err := f.Domain().Consensus().ValidateAndInsertBlock(block) if err != nil { if errors.As(err, &ruleerrors.RuleError{}) { diff --git a/app/protocol/flows/blockrelay/handle_relay_invs.go b/app/protocol/flows/blockrelay/handle_relay_invs.go index 9ac480c70..ff2bea87b 100644 --- a/app/protocol/flows/blockrelay/handle_relay_invs.go +++ b/app/protocol/flows/blockrelay/handle_relay_invs.go @@ -104,6 +104,11 @@ func (flow *handleRelayInvsFlow) start() error { continue } + err = flow.banIfBlockIsHeaderOnly(block) + if err != nil { + return err + } + log.Debugf("Processing block %s", inv.Hash) missingParents, blockInsertionResult, err := flow.processBlock(block) if err != nil { @@ -140,6 +145,15 @@ func (flow *handleRelayInvsFlow) start() error { } } +func (flow *handleRelayInvsFlow) banIfBlockIsHeaderOnly(block *externalapi.DomainBlock) error { + if len(block.Transactions) == 0 { + return protocolerrors.Errorf(true, "sent header of %s block where expected block with body", + consensushashing.BlockHash(block)) + } + + return nil +} + func (flow *handleRelayInvsFlow) readInv() (*appmessage.MsgInvRelayBlock, error) { if len(flow.invsQueue) > 0 { var inv *appmessage.MsgInvRelayBlock diff --git a/app/protocol/flows/blockrelay/ibd.go b/app/protocol/flows/blockrelay/ibd.go index e4851aed2..43a929796 100644 --- a/app/protocol/flows/blockrelay/ibd.go +++ b/app/protocol/flows/blockrelay/ibd.go @@ -533,6 +533,11 @@ func (flow *handleRelayInvsFlow) syncMissingBlockBodies(highHash *externalapi.Do return protocolerrors.Errorf(true, "expected block %s but got %s", expectedHash, blockHash) } + err = flow.banIfBlockIsHeaderOnly(block) + if err != nil { + return err + } + blockInsertionResult, err := flow.Domain().Consensus().ValidateAndInsertBlock(block) if err != nil { if errors.Is(err, ruleerrors.ErrDuplicateBlock) { diff --git a/app/protocol/flows/testing/handle_relay_invs_test.go b/app/protocol/flows/testing/handle_relay_invs_test.go index 36f28db13..e6fc2add7 100644 --- a/app/protocol/flows/testing/handle_relay_invs_test.go +++ b/app/protocol/flows/testing/handle_relay_invs_test.go @@ -24,6 +24,19 @@ import ( "github.com/pkg/errors" ) +var headerOnlyBlock = &externalapi.DomainBlock{ + Header: blockheader.NewImmutableBlockHeader( + constants.MaxBlockVersion, + []*externalapi.DomainHash{externalapi.NewDomainHashFromByteArray(&[externalapi.DomainHashSize]byte{1})}, + &externalapi.DomainHash{}, + &externalapi.DomainHash{}, + &externalapi.DomainHash{}, + 0, + 0, + 0, + ), +} + var orphanBlock = &externalapi.DomainBlock{ Header: blockheader.NewImmutableBlockHeader( constants.MaxBlockVersion, @@ -35,6 +48,7 @@ var orphanBlock = &externalapi.DomainBlock{ 0, 0, ), + Transactions: []*externalapi.DomainTransaction{{}}, } var validPruningPointBlock = &externalapi.DomainBlock{ @@ -48,6 +62,7 @@ var validPruningPointBlock = &externalapi.DomainBlock{ 0, 0, ), + Transactions: []*externalapi.DomainTransaction{{}}, } var invalidPruningPointBlock = &externalapi.DomainBlock{ @@ -61,6 +76,7 @@ var invalidPruningPointBlock = &externalapi.DomainBlock{ 0, 0, ), + Transactions: []*externalapi.DomainTransaction{{}}, } var unexpectedIBDBlock = &externalapi.DomainBlock{ @@ -74,6 +90,7 @@ var unexpectedIBDBlock = &externalapi.DomainBlock{ 0, 0, ), + Transactions: []*externalapi.DomainTransaction{{}}, } var invalidBlock = &externalapi.DomainBlock{ @@ -87,6 +104,7 @@ var invalidBlock = &externalapi.DomainBlock{ 0, 0, ), + Transactions: []*externalapi.DomainTransaction{{}}, } var unknownBlockHash = externalapi.NewDomainHashFromByteArray(&[externalapi.DomainHashSize]byte{1}) @@ -95,6 +113,7 @@ var validPruningPointHash = consensushashing.BlockHash(validPruningPointBlock) var invalidBlockHash = consensushashing.BlockHash(invalidBlock) var invalidPruningPointHash = consensushashing.BlockHash(invalidPruningPointBlock) var orphanBlockHash = consensushashing.BlockHash(orphanBlock) +var headerOnlyBlockHash = consensushashing.BlockHash(headerOnlyBlock) type fakeRelayInvsContext struct { testName string @@ -450,6 +469,29 @@ func TestHandleRelayInvs(t *testing.T) { expectsBan: true, expectsErrToContain: "got unrequested block", }, + { + name: "sending header only block on relay", + funcToExecute: func(t *testing.T, incomingRoute, outgoingRoute *router.Route, context *fakeRelayInvsContext) { + err := incomingRoute.Enqueue(appmessage.NewMsgInvBlock(headerOnlyBlockHash)) + if err != nil { + t.Fatalf("Enqueue: %+v", err) + } + + msg, err := outgoingRoute.DequeueWithTimeout(time.Second) + if err != nil { + t.Fatalf("DequeueWithTimeout: %+v", err) + } + _ = msg.(*appmessage.MsgRequestRelayBlocks) + + err = incomingRoute.Enqueue(appmessage.DomainBlockToMsgBlock(headerOnlyBlock)) + if err != nil { + t.Fatalf("Enqueue: %+v", err) + } + }, + expectsProtocolError: true, + expectsBan: true, + expectsErrToContain: "block where expected block with body", + }, { name: "sending invalid block", funcToExecute: func(t *testing.T, incomingRoute, outgoingRoute *router.Route, context *fakeRelayInvsContext) { diff --git a/app/rpc/rpchandlers/get_blocks_test.go b/app/rpc/rpchandlers/get_blocks_test.go index b01320b22..0a4dd46d5 100644 --- a/app/rpc/rpchandlers/get_blocks_test.go +++ b/app/rpc/rpchandlers/get_blocks_test.go @@ -1,6 +1,10 @@ package rpchandlers_test import ( + "reflect" + "sort" + "testing" + "github.com/kaspanet/kaspad/app/appmessage" "github.com/kaspanet/kaspad/app/rpc/rpccontext" "github.com/kaspanet/kaspad/app/rpc/rpchandlers" @@ -12,9 +16,6 @@ import ( "github.com/kaspanet/kaspad/domain/dagconfig" "github.com/kaspanet/kaspad/domain/miningmanager" "github.com/kaspanet/kaspad/infrastructure/config" - "reflect" - "sort" - "testing" ) type fakeDomain struct { @@ -65,66 +66,79 @@ func TestHandleGetBlocks(t *testing.T) { return antipast } - upBfsOrder := make([]*externalapi.DomainHash, 0, 30) - selectedParent := params.GenesisHash - upBfsOrder = append(upBfsOrder, selectedParent) + // Create a DAG with the following structure: + // merging block + // / | \ + // split1 split2 split3 + // \ | / + // merging block + // / | \ + // split1 split2 split3 + // \ | / + // etc. + expectedOrder := make([]*externalapi.DomainHash, 0, 40) + mergingBlock := params.GenesisHash for i := 0; i < 10; i++ { - parents := make([]*externalapi.DomainHash, 0, 3) - for j := 0; j < 4; j++ { - blockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{selectedParent}, nil, nil) + splitBlocks := make([]*externalapi.DomainHash, 0, 3) + for j := 0; j < 3; j++ { + blockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{mergingBlock}, nil, nil) if err != nil { t.Fatalf("Failed adding block: %v", err) } - parents = append(parents, blockHash) - upBfsOrder = append(upBfsOrder, blockHash) + splitBlocks = append(splitBlocks, blockHash) } - selectedParent, _, err = tc.AddBlock(parents, nil, nil) + sort.Sort(sort.Reverse(testutils.NewTestGhostDAGSorter(splitBlocks, tc, t))) + restOfSplitBlocks, selectedParent := splitBlocks[:len(splitBlocks)-1], splitBlocks[len(splitBlocks)-1] + expectedOrder = append(expectedOrder, selectedParent) + expectedOrder = append(expectedOrder, restOfSplitBlocks...) + + mergingBlock, _, err = tc.AddBlock(splitBlocks, nil, nil) if err != nil { t.Fatalf("Failed adding block: %v", err) } - upBfsOrder = append(upBfsOrder, selectedParent) + expectedOrder = append(expectedOrder, mergingBlock) } virtualSelectedParent, err := tc.GetVirtualSelectedParent() if err != nil { t.Fatalf("Failed getting SelectedParent: %v", err) } - if !virtualSelectedParent.Equal(upBfsOrder[len(upBfsOrder)-1]) { - t.Fatalf("Expected %s to be selectedParent, instead found: %s", upBfsOrder[len(upBfsOrder)-1], virtualSelectedParent) + if !virtualSelectedParent.Equal(expectedOrder[len(expectedOrder)-1]) { + t.Fatalf("Expected %s to be selectedParent, instead found: %s", expectedOrder[len(expectedOrder)-1], virtualSelectedParent) } requestSelectedParent := getBlocks(virtualSelectedParent) if !reflect.DeepEqual(requestSelectedParent.BlockHashes, hashes.ToStrings([]*externalapi.DomainHash{virtualSelectedParent})) { - t.Fatalf("TestSyncManager_GetHashesBetween expected %v\n == \n%v", requestSelectedParent.BlockHashes, virtualSelectedParent) + t.Fatalf("TestHandleGetBlocks expected:\n%v\nactual:\n%v", virtualSelectedParent, requestSelectedParent.BlockHashes) } - for i, blockHash := range upBfsOrder { - expectedBlocks := filterAntiPast(blockHash, upBfsOrder) - // sort the slice in the order GetBlocks is returning them - sort.Sort(sort.Reverse(testutils.NewTestGhostDAGSorter(expectedBlocks, tc, t))) + for i, blockHash := range expectedOrder { + expectedBlocks := filterAntiPast(blockHash, expectedOrder) expectedBlocks = append([]*externalapi.DomainHash{blockHash}, expectedBlocks...) - blocks := getBlocks(blockHash) - if !reflect.DeepEqual(blocks.BlockHashes, hashes.ToStrings(expectedBlocks)) { - t.Fatalf("TestSyncManager_GetHashesBetween %d expected %s\n == \n%s", i, blocks.BlockHashes, hashes.ToStrings(expectedBlocks)) + actualBlocks := getBlocks(blockHash) + if !reflect.DeepEqual(actualBlocks.BlockHashes, hashes.ToStrings(expectedBlocks)) { + t.Fatalf("TestHandleGetBlocks %d \nexpected: \n%v\nactual:\n%v", i, + hashes.ToStrings(expectedBlocks), actualBlocks.BlockHashes) } } - // Make explitly sure that if lowHash==highHash we get a slice with a single hash. - blocks := getBlocks(virtualSelectedParent) - if !reflect.DeepEqual(blocks.BlockHashes, []string{virtualSelectedParent.String()}) { - t.Fatalf("TestSyncManager_GetHashesBetween expected blocks to contain just '%s', instead got: \n%s", virtualSelectedParent, blocks.BlockHashes) + // Make explicitly sure that if lowHash==highHash we get a slice with a single hash. + actualBlocks := getBlocks(virtualSelectedParent) + if !reflect.DeepEqual(actualBlocks.BlockHashes, []string{virtualSelectedParent.String()}) { + t.Fatalf("TestHandleGetBlocks expected blocks to contain just '%s', instead got: \n%v", + virtualSelectedParent, actualBlocks.BlockHashes) } - sort.Sort(sort.Reverse(testutils.NewTestGhostDAGSorter(upBfsOrder, tc, t))) - requestAllViaNil := getBlocks(nil) - if !reflect.DeepEqual(requestAllViaNil.BlockHashes, hashes.ToStrings(upBfsOrder)) { - t.Fatalf("TestSyncManager_GetHashesBetween expected %v\n == \n%v", requestAllViaNil.BlockHashes, upBfsOrder) + expectedOrder = append([]*externalapi.DomainHash{params.GenesisHash}, expectedOrder...) + actualOrder := getBlocks(nil) + if !reflect.DeepEqual(actualOrder.BlockHashes, hashes.ToStrings(expectedOrder)) { + t.Fatalf("TestHandleGetBlocks \nexpected: %v \nactual:\n%v", expectedOrder, actualOrder.BlockHashes) } requestAllExplictly := getBlocks(params.GenesisHash) - if !reflect.DeepEqual(requestAllExplictly.BlockHashes, hashes.ToStrings(upBfsOrder)) { - t.Fatalf("TestSyncManager_GetHashesBetween expected %v\n == \n%v", requestAllExplictly.BlockHashes, upBfsOrder) + if !reflect.DeepEqual(requestAllExplictly.BlockHashes, hashes.ToStrings(expectedOrder)) { + t.Fatalf("TestHandleGetBlocks \nexpected: \n%v\n. actual:\n%v", expectedOrder, requestAllExplictly.BlockHashes) } }) } diff --git a/app/rpc/rpchandlers/submit_block.go b/app/rpc/rpchandlers/submit_block.go index 475e2c00c..373eeb2f5 100644 --- a/app/rpc/rpchandlers/submit_block.go +++ b/app/rpc/rpchandlers/submit_block.go @@ -2,6 +2,7 @@ package rpchandlers import ( "github.com/kaspanet/kaspad/app/appmessage" + "github.com/kaspanet/kaspad/app/protocol/protocolerrors" "github.com/kaspanet/kaspad/app/rpc/rpccontext" "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" @@ -25,9 +26,10 @@ func HandleSubmitBlock(context *rpccontext.Context, _ *router.Router, request ap err := context.ProtocolManager.AddBlock(domainBlock) if err != nil { - if !errors.As(err, &ruleerrors.RuleError{}) { + if !errors.As(err, &ruleerrors.RuleError{}) || !errors.As(err, &protocolerrors.ProtocolError{}) { return nil, err } + return &appmessage.SubmitBlockResponseMessage{ Error: appmessage.RPCErrorf("Block rejected. Reason: %s", err), RejectReason: appmessage.RejectReasonBlockInvalid, diff --git a/cmd/kaspactl/README.md b/cmd/kaspactl/README.md index 01f1e2c2b..cf0fcf7b5 100644 --- a/cmd/kaspactl/README.md +++ b/cmd/kaspactl/README.md @@ -4,7 +4,7 @@ kaspactl is an RPC client for kaspad ## Requirements -Go 1.15 or later. +Go 1.16 or later. ## Installation diff --git a/cmd/kaspactl/docker/Dockerfile b/cmd/kaspactl/docker/Dockerfile index 2dae764d4..ada893150 100644 --- a/cmd/kaspactl/docker/Dockerfile +++ b/cmd/kaspactl/docker/Dockerfile @@ -1,5 +1,5 @@ # -- multistage docker build: stage #1: build stage -FROM golang:1.15-alpine AS build +FROM golang:1.16-alpine AS build RUN mkdir -p /go/src/github.com/kaspanet/kaspad diff --git a/cmd/kaspaminer/README.md b/cmd/kaspaminer/README.md index acdd51b5c..26bdfa61f 100644 --- a/cmd/kaspaminer/README.md +++ b/cmd/kaspaminer/README.md @@ -4,7 +4,7 @@ Kaspaminer is a CPU-based miner for kaspad ## Requirements -Go 1.15 or later. +Go 1.16 or later. ## Installation diff --git a/cmd/kaspaminer/docker/Dockerfile b/cmd/kaspaminer/docker/Dockerfile index 3a7758ae2..63356a7e6 100644 --- a/cmd/kaspaminer/docker/Dockerfile +++ b/cmd/kaspaminer/docker/Dockerfile @@ -1,5 +1,5 @@ # -- multistage docker build: stage #1: build stage -FROM golang:1.15-alpine AS build +FROM golang:1.16-alpine AS build RUN mkdir -p /go/src/github.com/kaspanet/kaspad diff --git a/cmd/kaspaminer/mineloop.go b/cmd/kaspaminer/mineloop.go index 43d62f429..f449f4481 100644 --- a/cmd/kaspaminer/mineloop.go +++ b/cmd/kaspaminer/mineloop.go @@ -2,13 +2,14 @@ package main import ( nativeerrors "errors" - "github.com/kaspanet/kaspad/cmd/kaspaminer/templatemanager" - "github.com/kaspanet/kaspad/domain/consensus/model/pow" - "github.com/kaspanet/kaspad/util/difficulty" "math/rand" "sync/atomic" "time" + "github.com/kaspanet/kaspad/cmd/kaspaminer/templatemanager" + "github.com/kaspanet/kaspad/domain/consensus/model/pow" + "github.com/kaspanet/kaspad/util/difficulty" + "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" @@ -155,11 +156,15 @@ func mineNextBlock(mineWhenNotSynced bool) *externalapi.DomainBlock { func getBlockForMining(mineWhenNotSynced bool) *externalapi.DomainBlock { tryCount := 0 + + const sleepTime = 500 * time.Millisecond + const sleepTimeWhenNotSynced = 5 * time.Second + for { tryCount++ - const sleepTime = 500 * time.Millisecond + shouldLog := (tryCount-1)%10 == 0 - template := templatemanager.Get() + template, isSynced := templatemanager.Get() if template == nil { if shouldLog { log.Info("Waiting for the initial template") @@ -167,15 +172,15 @@ func getBlockForMining(mineWhenNotSynced bool) *externalapi.DomainBlock { time.Sleep(sleepTime) continue } - if !template.IsSynced && !mineWhenNotSynced { + if !isSynced && !mineWhenNotSynced { if shouldLog { log.Warnf("Kaspad is not synced. Skipping current block template") } - time.Sleep(sleepTime) + time.Sleep(sleepTimeWhenNotSynced) continue } - return appmessage.MsgBlockToDomainBlock(template.MsgBlock) + return template } } diff --git a/cmd/kaspaminer/templatemanager/templatemanager.go b/cmd/kaspaminer/templatemanager/templatemanager.go index 96eddfe67..438050d2a 100644 --- a/cmd/kaspaminer/templatemanager/templatemanager.go +++ b/cmd/kaspaminer/templatemanager/templatemanager.go @@ -2,22 +2,31 @@ package templatemanager import ( "github.com/kaspanet/kaspad/app/appmessage" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "sync" ) -var currentTemplate *appmessage.GetBlockTemplateResponseMessage +var currentTemplate *externalapi.DomainBlock +var isSynced bool var lock = &sync.Mutex{} // Get returns the template to work on -func Get() *appmessage.GetBlockTemplateResponseMessage { +func Get() (*externalapi.DomainBlock, bool) { lock.Lock() defer lock.Unlock() - return currentTemplate + // Shallow copy the block so when the user replaces the header it won't affect the template here. + if currentTemplate == nil { + return nil, false + } + block := *currentTemplate + return &block, isSynced } // Set sets the current template to work on func Set(template *appmessage.GetBlockTemplateResponseMessage) { + block := appmessage.MsgBlockToDomainBlock(template.MsgBlock) lock.Lock() defer lock.Unlock() - currentTemplate = template + currentTemplate = block + isSynced = template.IsSynced } diff --git a/cmd/wallet/README.md b/cmd/wallet/README.md index 36cd5f580..f0d9ce982 100644 --- a/cmd/wallet/README.md +++ b/cmd/wallet/README.md @@ -10,7 +10,7 @@ It is capable of generating wallet key-pairs, printing a wallet's current balanc ## Requirements -Go 1.15 or later. +Go 1.16 or later. ## Installation diff --git a/docker/Dockerfile b/docker/Dockerfile index 1a00521e1..a503009d3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # -- multistage docker build: stage #1: build stage -FROM golang:1.15-alpine AS build +FROM golang:1.16-alpine AS build RUN mkdir -p /go/src/github.com/kaspanet/kaspad @@ -16,9 +16,12 @@ RUN go get -u golang.org/x/lint/golint \ COPY go.mod . COPY go.sum . +# Cache kaspad dependencies +RUN go mod download + COPY . . -RUN NO_PARALLEL=1 ./build_and_test.sh +RUN ./build_and_test.sh # --- multistage docker build: stage #2: runtime image FROM alpine diff --git a/domain/consensus/constructors.go b/domain/consensus/constructors.go index 29b7a2167..38b91ed89 100644 --- a/domain/consensus/constructors.go +++ b/domain/consensus/constructors.go @@ -1,6 +1,21 @@ package consensus -import "github.com/kaspanet/kaspad/domain/consensus/model" +import ( + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "math/big" + "time" +) // GHOSTDAGManagerConstructor is the function signature for a constructor of a type implementing model.GHOSTDAGManager -type GHOSTDAGManagerConstructor func(model.DBReader, model.DAGTopologyManager, model.GHOSTDAGDataStore, model.BlockHeaderStore, model.KType) model.GHOSTDAGManager +type GHOSTDAGManagerConstructor func(model.DBReader, model.DAGTopologyManager, + model.GHOSTDAGDataStore, model.BlockHeaderStore, model.KType) model.GHOSTDAGManager + +// DifficultyManagerConstructor is the function signature for a constructor of a type implementing model.DifficultyManager +type DifficultyManagerConstructor func(model.DBReader, model.GHOSTDAGManager, model.GHOSTDAGDataStore, + model.BlockHeaderStore, model.DAGTopologyManager, model.DAGTraversalManager, *big.Int, int, bool, time.Duration, + *externalapi.DomainHash) model.DifficultyManager + +// PastMedianTimeManagerConstructor is the function signature for a constructor of a type implementing model.PastMedianTimeManager +type PastMedianTimeManagerConstructor func(int, model.DBReader, model.DAGTraversalManager, model.BlockHeaderStore, + model.GHOSTDAGDataStore) model.PastMedianTimeManager diff --git a/domain/consensus/factory.go b/domain/consensus/factory.go index 52c999e4c..c609159e5 100644 --- a/domain/consensus/factory.go +++ b/domain/consensus/factory.go @@ -63,19 +63,25 @@ type Factory interface { SetTestGHOSTDAGManager(ghostdagConstructor GHOSTDAGManagerConstructor) SetTestLevelDBCacheSize(cacheSizeMiB int) SetTestPreAllocateCache(preallocateCaches bool) + SetTestPastMedianTimeManager(medianTimeConstructor PastMedianTimeManagerConstructor) + SetTestDifficultyManager(difficultyConstructor DifficultyManagerConstructor) } type factory struct { - dataDir string - ghostdagConstructor GHOSTDAGManagerConstructor - cacheSizeMiB *int - preallocateCaches *bool + dataDir string + ghostdagConstructor GHOSTDAGManagerConstructor + pastMedianTimeConsructor PastMedianTimeManagerConstructor + difficultyConstructor DifficultyManagerConstructor + cacheSizeMiB *int + preallocateCaches *bool } // NewFactory creates a new Consensus factory func NewFactory() Factory { return &factory{ - ghostdagConstructor: ghostdagmanager.New, + ghostdagConstructor: ghostdagmanager.New, + pastMedianTimeConsructor: pastmediantimemanager.New, + difficultyConstructor: difficultymanager.New, } } @@ -144,7 +150,7 @@ func (f *factory) NewConsensus(dagParams *dagconfig.Params, db infrastructuredat reachabilityDataStore, ghostdagManager, consensusStateStore) - pastMedianTimeManager := pastmediantimemanager.New( + pastMedianTimeManager := f.pastMedianTimeConsructor( dagParams.TimestampDeviationTolerance, dbManager, dagTraversalManager, @@ -159,7 +165,7 @@ func (f *factory) NewConsensus(dagParams *dagconfig.Params, db infrastructuredat dbManager, pastMedianTimeManager, ghostdagDataStore) - difficultyManager := difficultymanager.New( + difficultyManager := f.difficultyConstructor( dbManager, ghostdagManager, ghostdagDataStore, @@ -466,6 +472,15 @@ func (f *factory) SetTestGHOSTDAGManager(ghostdagConstructor GHOSTDAGManagerCons f.ghostdagConstructor = ghostdagConstructor } +func (f *factory) SetTestPastMedianTimeManager(medianTimeConstructor PastMedianTimeManagerConstructor) { + f.pastMedianTimeConsructor = medianTimeConstructor +} + +// SetTestDifficultyManager is a setter for the difficultyManager field on the factory. +func (f *factory) SetTestDifficultyManager(difficultyConstructor DifficultyManagerConstructor) { + f.difficultyConstructor = difficultyConstructor +} + func (f *factory) SetTestLevelDBCacheSize(cacheSizeMiB int) { f.cacheSizeMiB = &cacheSizeMiB } diff --git a/domain/consensus/log.go b/domain/consensus/log.go index 520dcce8d..19fbdc9ac 100644 --- a/domain/consensus/log.go +++ b/domain/consensus/log.go @@ -2,6 +2,8 @@ package consensus import ( "github.com/kaspanet/kaspad/infrastructure/logger" + "github.com/kaspanet/kaspad/util/panics" ) var log, _ = logger.Get(logger.SubsystemTags.BDAG) +var spawn = panics.GoroutineWrapperFunc(log) diff --git a/domain/consensus/model/interface_processes_dagtraversalmanager.go b/domain/consensus/model/interface_processes_dagtraversalmanager.go index 9f06135d8..9101da297 100644 --- a/domain/consensus/model/interface_processes_dagtraversalmanager.go +++ b/domain/consensus/model/interface_processes_dagtraversalmanager.go @@ -11,7 +11,7 @@ type DAGTraversalManager interface { // from lowHash (exclusive) to highHash (inclusive) over highHash's selected parent chain SelectedChildIterator(highHash, lowHash *externalapi.DomainHash) (BlockIterator, error) Anticone(blockHash *externalapi.DomainHash) ([]*externalapi.DomainHash, error) - BlueWindow(highHash *externalapi.DomainHash, windowSize int) ([]*externalapi.DomainHash, error) + BlockWindow(highHash *externalapi.DomainHash, windowSize int) ([]*externalapi.DomainHash, error) NewDownHeap() BlockHeap NewUpHeap() BlockHeap CalculateChainPath( diff --git a/domain/consensus/model/testapi/test_consensus.go b/domain/consensus/model/testapi/test_consensus.go index a51991455..dd14b5322 100644 --- a/domain/consensus/model/testapi/test_consensus.go +++ b/domain/consensus/model/testapi/test_consensus.go @@ -1,11 +1,12 @@ package testapi import ( + "io" + "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/dagconfig" "github.com/kaspanet/kaspad/infrastructure/db/database" - "io" ) // MineJSONBlockType indicates which type of blocks MineJSON mines @@ -50,6 +51,8 @@ type TestConsensus interface { MineJSON(r io.Reader, blockType MineJSONBlockType) (tips []*externalapi.DomainHash, err error) DiscardAllStores() + RenderDAGToDot(filename string) error + AcceptanceDataStore() model.AcceptanceDataStore BlockHeaderStore() model.BlockHeaderStore BlockRelationStore() model.BlockRelationStore diff --git a/domain/consensus/processes/blockprocessor/blocklogger/blocklogger.go b/domain/consensus/processes/blockprocessor/blocklogger/blocklogger.go deleted file mode 100644 index c012f82ba..000000000 --- a/domain/consensus/processes/blockprocessor/blocklogger/blocklogger.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015-2017 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package blocklogger - -import ( - "time" - - "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" - "github.com/kaspanet/kaspad/util/mstime" -) - -var ( - receivedLogBlocks int64 - receivedLogHeaders int64 - receivedLogTransactions int64 - lastBlockLogTime time.Time -) - -// LogBlock logs a new block blue score as an information message -// to show progress to the user. In order to prevent spam, it limits logging to -// one message every 10 seconds with duration and totals included. -func LogBlock(block *externalapi.DomainBlock) { - if len(block.Transactions) == 0 { - receivedLogHeaders++ - } else { - receivedLogBlocks++ - } - - receivedLogTransactions += int64(len(block.Transactions)) - - now := time.Now() - duration := now.Sub(lastBlockLogTime) - if duration < time.Second*10 { - return - } - - // Truncate the duration to 10s of milliseconds. - truncatedDuration := duration.Round(10 * time.Millisecond) - - // Log information about new block blue score. - blockStr := "blocks" - if receivedLogBlocks == 1 { - blockStr = "block" - } - - txStr := "transactions" - if receivedLogTransactions == 1 { - txStr = "transaction" - } - - headerStr := "headers" - if receivedLogBlocks == 1 { - headerStr = "header" - } - - log.Infof("Processed %d %s and %d %s in the last %s (%d %s, %s)", - receivedLogBlocks, blockStr, receivedLogHeaders, headerStr, truncatedDuration, receivedLogTransactions, - txStr, mstime.UnixMilliseconds(block.Header.TimeInMilliseconds())) - - receivedLogBlocks = 0 - receivedLogHeaders = 0 - receivedLogTransactions = 0 - lastBlockLogTime = now -} diff --git a/domain/consensus/processes/blockprocessor/blockprocessor.go b/domain/consensus/processes/blockprocessor/blockprocessor.go index 100874ce3..1d67188ac 100644 --- a/domain/consensus/processes/blockprocessor/blockprocessor.go +++ b/domain/consensus/processes/blockprocessor/blockprocessor.go @@ -3,6 +3,7 @@ package blockprocessor import ( "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/domain/consensus/processes/blockprocessor/blocklogger" "github.com/kaspanet/kaspad/infrastructure/logger" "time" ) @@ -13,6 +14,7 @@ type blockProcessor struct { genesisHash *externalapi.DomainHash targetTimePerBlock time.Duration databaseContext model.DBManager + blockLogger *blocklogger.BlockLogger consensusStateManager model.ConsensusStateManager pruningManager model.PruningManager @@ -49,6 +51,7 @@ func New( genesisHash *externalapi.DomainHash, targetTimePerBlock time.Duration, databaseContext model.DBManager, + consensusStateManager model.ConsensusStateManager, pruningManager model.PruningManager, blockValidator model.BlockValidator, @@ -81,6 +84,7 @@ func New( genesisHash: genesisHash, targetTimePerBlock: targetTimePerBlock, databaseContext: databaseContext, + blockLogger: blocklogger.NewBlockLogger(), pruningManager: pruningManager, blockValidator: blockValidator, dagTopologyManager: dagTopologyManager, diff --git a/domain/consensus/processes/blockprocessor/validateandinsertblock.go b/domain/consensus/processes/blockprocessor/validateandinsertblock.go index c7a051f2a..7a4b5a055 100644 --- a/domain/consensus/processes/blockprocessor/validateandinsertblock.go +++ b/domain/consensus/processes/blockprocessor/validateandinsertblock.go @@ -2,7 +2,6 @@ package blockprocessor import ( "fmt" - "github.com/kaspanet/kaspad/domain/consensus/processes/blockprocessor/blocklogger" "github.com/kaspanet/kaspad/util/difficulty" "github.com/kaspanet/kaspad/domain/consensus/model" @@ -144,7 +143,7 @@ func (bp *blockProcessor) validateAndInsertBlock(block *externalapi.DomainBlock, return nil, logClosureErr } - blocklogger.LogBlock(block) + bp.blockLogger.LogBlock(block) return &externalapi.BlockInsertionResult{ VirtualSelectedParentChainChanges: selectedParentChainChanges, diff --git a/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go b/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go index 847ab3a0d..b7e375626 100644 --- a/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go +++ b/domain/consensus/processes/blockvalidator/block_body_in_isolation_test.go @@ -1,6 +1,12 @@ package blockvalidator_test import ( + "bytes" + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/testapi" + "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/kaspanet/kaspad/domain/consensus/utils/merkle" + "github.com/kaspanet/kaspad/domain/consensus/utils/utxo" "math" "testing" @@ -84,7 +90,7 @@ func TestChainedTransactions(t *testing.T) { func TestCheckBlockSanity(t *testing.T) { testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { factory := consensus.NewFactory() - consensus, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockSanity") + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockSanity") if err != nil { t.Fatalf("Error setting up consensus: %+v", err) } @@ -94,17 +100,17 @@ func TestCheckBlockSanity(t *testing.T) { t.Fatalf("Too few transactions in block, expect at least 3, got %v", len(exampleValidBlock.Transactions)) } - consensus.BlockStore().Stage(blockHash, &exampleValidBlock) + tc.BlockStore().Stage(blockHash, &exampleValidBlock) - err = consensus.BlockValidator().ValidateBodyInIsolation(blockHash) + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) if err != nil { t.Fatalf("Failed validating block in isolation: %v", err) } // Test with block with wrong transactions sorting order blockHash = consensushashing.BlockHash(&blockWithWrongTxOrder) - consensus.BlockStore().Stage(blockHash, &blockWithWrongTxOrder) - err = consensus.BlockValidator().ValidateBodyInIsolation(blockHash) + tc.BlockStore().Stage(blockHash, &blockWithWrongTxOrder) + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) if !errors.Is(err, ruleerrors.ErrTransactionsNotSorted) { t.Errorf("CheckBlockSanity: Expected ErrTransactionsNotSorted error, instead got %v", err) } @@ -112,8 +118,8 @@ func TestCheckBlockSanity(t *testing.T) { // Test a block with invalid parents order // We no longer require blocks to have ordered parents blockHash = consensushashing.BlockHash(&unOrderedParentsBlock) - consensus.BlockStore().Stage(blockHash, &unOrderedParentsBlock) - err = consensus.BlockValidator().ValidateBodyInIsolation(blockHash) + tc.BlockStore().Stage(blockHash, &unOrderedParentsBlock) + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) if err != nil { t.Errorf("CheckBlockSanity: Expected block to be be body in isolation valid, got error instead: %v", err) } @@ -1034,3 +1040,309 @@ func TestCheckBlockHashMerkleRoot(t *testing.T) { } }) } + +func TestBlockSize(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestBlockSize") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block, _, err := initBlockWithInvalidBlockSize(params, tc) + if err != nil { + t.Fatalf("Error BuildBlockWithParents : %+v", err) + } + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrBlockSizeTooHigh) { + t.Fatalf("ValidateBodyInIsolationTest: TestBlockSize:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrBlockSizeTooHigh, err) + } + }) +} + +func initBlockWithInvalidBlockSize(params *dagconfig.Params, tc testapi.TestConsensus) (*externalapi.DomainBlock, model.UTXODiff, error) { + emptyCoinbase := externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{ + Script: nil, + Version: 0, + }, + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + bigSignatureScript := bytes.Repeat([]byte("01"), 25000) + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bigSignatureScript, + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(5)), + } + tx := &externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + PayloadHash: *externalapi.NewDomainHashFromByteArray(&[externalapi.DomainHashSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), + Payload: []byte{0x01}, + } + + return tc.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, &emptyCoinbase, []*externalapi.DomainTransaction{tx}) +} + +func TestCheckBlockDuplicateTransactions(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockDuplicateTransactions") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block, _, err := initBlockWithDuplicateTransaction(params, tc) + if err != nil { + t.Fatalf("Error BuildBlockWithParents : %+v", err) + } + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrDuplicateTx) { + t.Fatalf("ValidateBodyInIsolationTest: TestCheckBlockDuplicateTransactions:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrDuplicateTx, err) + } + }) +} + +func initBlockWithDuplicateTransaction(params *dagconfig.Params, tc testapi.TestConsensus) (*externalapi.DomainBlock, model.UTXODiff, error) { + emptyCoinbase := externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{ + Script: nil, + Version: 0, + }, + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("01"), 10), + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(5)), + } + tx := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDNative, + } + + return tc.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, &emptyCoinbase, []*externalapi.DomainTransaction{tx, tx}) +} + +func TestCheckBlockContainsOnlyOneCoinbase(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockContainsOnlyOneCoinbase") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block, _, err := initBlockWithMoreThanOneCoinbase(params, tc) + if err != nil { + t.Fatalf("Error BuildBlockWithParents : %+v", err) + } + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrMultipleCoinbases) { + t.Fatalf("ValidateBodyInIsolationTest: TestCheckBlockContainsOnlyOneCoinbase:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrMultipleCoinbases, err) + } + }) +} + +func initBlockWithMoreThanOneCoinbase(params *dagconfig.Params, tc testapi.TestConsensus) (*externalapi.DomainBlock, model.UTXODiff, error) { + emptyCoinbase := externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{ + Script: nil, + Version: 0, + }, + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("01"), 10), + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(5)), + } + tx := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDCoinbase, + } + + return tc.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, &emptyCoinbase, []*externalapi.DomainTransaction{tx}) +} + +func TestCheckBlockDoubleSpends(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckBlockDoubleSpends") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block, _, err := initBlockWithDoubleSpends(params, tc) + if err != nil { + t.Fatalf("Error BuildBlockWithParents : %+v", err) + } + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrDoubleSpendInSameBlock) { + t.Fatalf("ValidateBodyInIsolationTest: TestCheckBlockDoubleSpends:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrDoubleSpendInSameBlock, err) + } + }) +} + +func initBlockWithDoubleSpends(params *dagconfig.Params, tc testapi.TestConsensus) (*externalapi.DomainBlock, model.UTXODiff, error) { + emptyCoinbase := externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{ + Script: nil, + Version: 0, + }, + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("01"), 10), + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(5)), + } + tx := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDNative, + } + txInputSameOutpoint := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("02"), 10), + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, + &externalapi.ScriptPublicKey{}, + true, + uint64(4)), + } + txSameOutpoint := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInputSameOutpoint}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDNative, + } + + return tc.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, + &emptyCoinbase, []*externalapi.DomainTransaction{tx, txSameOutpoint}) +} + +func TestCheckFirstBlockTransactionIsCoinbase(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestCheckFirstBlockTransactionIsCoinbase") + if err != nil { + t.Fatalf("Error setting up tc: %+v", err) + } + defer teardown(false) + + block := initBlockWithFirstTransactionDifferentThanCoinbase(params) + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + + err = tc.BlockValidator().ValidateBodyInIsolation(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrFirstTxNotCoinbase) { + t.Fatalf("ValidateBodyInIsolationTest: TestCheckFirstBlockTransactionIsCoinbase:"+ + " Unexpected error: Expected to: %v, but got : %v", ruleerrors.ErrFirstTxNotCoinbase, err) + } + }) +} + +func initBlockWithFirstTransactionDifferentThanCoinbase(params *dagconfig.Params) *externalapi.DomainBlock { + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: bytes.Repeat([]byte("01"), 10), + Sequence: constants.MaxTxInSequenceNum, + } + tx := &externalapi.DomainTransaction{ + Version: 0, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{{uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 2}, Version: 0}}, {uint64(0xFFFF), + &externalapi.ScriptPublicKey{Script: []byte{1, 3}, Version: 0}}}, + SubnetworkID: subnetworks.SubnetworkIDNative, + } + + return &externalapi.DomainBlock{ + Header: blockheader.NewImmutableBlockHeader( + constants.MaxBlockVersion, + []*externalapi.DomainHash{params.GenesisHash}, + merkle.CalculateHashMerkleRoot([]*externalapi.DomainTransaction{tx}), + &externalapi.DomainHash{}, + externalapi.NewDomainHashFromByteArray(&[externalapi.DomainHashSize]byte{ + 0x80, 0xf7, 0x00, 0xe3, 0x16, 0x3d, 0x04, 0x95, + 0x5b, 0x7e, 0xaf, 0x84, 0x7e, 0x1b, 0x6b, 0x06, + 0x4e, 0x06, 0xba, 0x64, 0xd7, 0x61, 0xda, 0x25, + 0x1a, 0x0e, 0x21, 0xd4, 0x64, 0x49, 0x02, 0xa2, + }), + 0x5cd18053000, + 0x207fffff, + 0x1), + Transactions: []*externalapi.DomainTransaction{tx}, + } +} diff --git a/domain/consensus/processes/blockvalidator/pruning_violation_proof_of_work_and_difficulty_test.go b/domain/consensus/processes/blockvalidator/pruning_violation_proof_of_work_and_difficulty_test.go index a78d355dc..9a785ed8a 100644 --- a/domain/consensus/processes/blockvalidator/pruning_violation_proof_of_work_and_difficulty_test.go +++ b/domain/consensus/processes/blockvalidator/pruning_violation_proof_of_work_and_difficulty_test.go @@ -1,6 +1,7 @@ package blockvalidator_test import ( + "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model/pow" "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" "github.com/kaspanet/kaspad/domain/consensus/utils/blockheader" @@ -12,8 +13,8 @@ import ( "math" "math/big" "math/rand" - "testing" + "time" "github.com/kaspanet/kaspad/domain/consensus" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" @@ -241,3 +242,60 @@ func TestCheckPruningPointViolation(t *testing.T) { } }) } + +// TestValidateDifficulty verifies that in case of a block with an unexpected difficulty, +// an appropriate error message (ErrUnexpectedDifficulty) will be returned on the +// function ValidatePruningPointViolationAndProofOfWorkAndDifficulty. The required difficulty is +// "calculated" by the function (dm *mocDifficultyManager) RequiredDifficulty , +// where mocDifficultyManager is special implementation of the type DifficultyManager for this test (defined below). +func TestValidateDifficulty(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + mocDifficulty := &mocDifficultyManager{} + factory.SetTestDifficultyManager(func(model.DBReader, model.GHOSTDAGManager, model.GHOSTDAGDataStore, + model.BlockHeaderStore, model.DAGTopologyManager, model.DAGTraversalManager, *big.Int, int, bool, time.Duration, + *externalapi.DomainHash) model.DifficultyManager { + return mocDifficulty + }) + genesisDifficulty := params.GenesisBlock.Header.Bits() + mocDifficulty.testDifficulty = genesisDifficulty + mocDifficulty.testGenesisBits = genesisDifficulty + tc, teardown, err := factory.NewTestConsensus(params, false, "TestValidateDifficulty") + if err != nil { + t.Fatalf("Error setting up consensus: %+v", err) + } + defer teardown(false) + + emptyCoinbase := externalapi.DomainCoinbaseData{ + ScriptPublicKey: &externalapi.ScriptPublicKey{ + Script: nil, + Version: 0, + }, + } + block, _, err := tc.BuildBlockWithParents([]*externalapi.DomainHash{params.GenesisHash}, &emptyCoinbase, nil) + if err != nil { + t.Fatalf("TestValidateDifficulty: Failed build block with parents: %v.", err) + } + blockHash := consensushashing.BlockHash(block) + tc.BlockStore().Stage(blockHash, block) + tc.BlockHeaderStore().Stage(blockHash, block.Header) + wrongTestDifficulty := mocDifficulty.testDifficulty + uint32(5) + mocDifficulty.testDifficulty = wrongTestDifficulty + + err = tc.BlockValidator().ValidatePruningPointViolationAndProofOfWorkAndDifficulty(blockHash) + if err == nil || !errors.Is(err, ruleerrors.ErrUnexpectedDifficulty) { + t.Fatalf("Expected block to be invalid with err: %v, instead found: %v", ruleerrors.ErrUnexpectedDifficulty, err) + } + }) +} + +type mocDifficultyManager struct { + testDifficulty uint32 + testGenesisBits uint32 +} + +// RequiredDifficulty returns the difficulty required for the test +func (dm *mocDifficultyManager) RequiredDifficulty(*externalapi.DomainHash) (uint32, error) { + return dm.testDifficulty, nil +} diff --git a/domain/consensus/processes/dagtraversalmanager/window.go b/domain/consensus/processes/dagtraversalmanager/window.go index a8d806a78..cff3f6906 100644 --- a/domain/consensus/processes/dagtraversalmanager/window.go +++ b/domain/consensus/processes/dagtraversalmanager/window.go @@ -4,11 +4,11 @@ import ( "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" ) -// blueBlockWindow returns a blockWindow of the given size that contains the -// blues in the past of startindNode, the sorting is unspecified. -// If the number of blues in the past of startingNode is less then windowSize, +// BlockWindow returns a blockWindow of the given size that contains the +// blocks in the past of startindNode, the sorting is unspecified. +// If the number of blocks in the past of startingNode is less then windowSize, // the window will be padded by genesis blocks to achieve a size of windowSize. -func (dtm *dagTraversalManager) BlueWindow(startingBlock *externalapi.DomainHash, windowSize int) ([]*externalapi.DomainHash, error) { +func (dtm *dagTraversalManager) BlockWindow(startingBlock *externalapi.DomainHash, windowSize int) ([]*externalapi.DomainHash, error) { currentHash := startingBlock currentGHOSTDAGData, err := dtm.ghostdagDataStore.Get(dtm.databaseContext, currentHash) if err != nil { diff --git a/domain/consensus/processes/dagtraversalmanager/window_test.go b/domain/consensus/processes/dagtraversalmanager/window_test.go index 399e434a7..5f4db12c2 100644 --- a/domain/consensus/processes/dagtraversalmanager/window_test.go +++ b/domain/consensus/processes/dagtraversalmanager/window_test.go @@ -13,7 +13,7 @@ import ( "github.com/pkg/errors" ) -func TestBlueBlockWindow(t *testing.T) { +func TestBlockWindow(t *testing.T) { tests := map[string][]*struct { parents []string id string //id is a virtual entity that is used only for tests so we can define relations between blocks without knowing their hash @@ -311,7 +311,7 @@ func TestBlueBlockWindow(t *testing.T) { testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { params.K = 1 factory := consensus.NewFactory() - tc, tearDown, err := factory.NewTestConsensus(params, false, "TestBlueBlockWindow") + tc, tearDown, err := factory.NewTestConsensus(params, false, "TestBlockWindow") if err != nil { t.Fatalf("NewTestConsensus: %s", err) } @@ -340,9 +340,9 @@ func TestBlueBlockWindow(t *testing.T) { blockByIDMap[blockData.id] = block idByBlockMap[*block] = blockData.id - window, err := tc.DAGTraversalManager().BlueWindow(block, windowSize) + window, err := tc.DAGTraversalManager().BlockWindow(block, windowSize) if err != nil { - t.Fatalf("BlueWindow: %s", err) + t.Fatalf("BlockWindow: %s", err) } sort.Sort(testutils.NewTestGhostDAGSorter(window, tc, t)) if err := checkWindowIDs(window, blockData.expectedWindowWithGenesisPadding, idByBlockMap); err != nil { diff --git a/domain/consensus/processes/difficultymanager/blockwindow.go b/domain/consensus/processes/difficultymanager/blockwindow.go index d6c685c93..f73d4c14e 100644 --- a/domain/consensus/processes/difficultymanager/blockwindow.go +++ b/domain/consensus/processes/difficultymanager/blockwindow.go @@ -1,12 +1,13 @@ package difficultymanager import ( - "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" - "github.com/kaspanet/kaspad/util/difficulty" - "github.com/pkg/errors" "math" "math/big" "sort" + + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/util/difficulty" + "github.com/pkg/errors" ) type difficultyBlock struct { @@ -27,13 +28,13 @@ func (dm *difficultyManager) getDifficultyBlock(blockHash *externalapi.DomainHas }, nil } -// blueBlockWindow returns a blockWindow of the given size that contains the -// blues in the past of startindNode, the sorting is unspecified. -// If the number of blues in the past of startingNode is less then windowSize, +// blockWindow returns a blockWindow of the given size that contains the +// blocks in the past of startindNode, the sorting is unspecified. +// If the number of blocks in the past of startingNode is less then windowSize, // the window will be padded by genesis blocks to achieve a size of windowSize. -func (dm *difficultyManager) blueBlockWindow(startingNode *externalapi.DomainHash, windowSize int) (blockWindow, error) { +func (dm *difficultyManager) blockWindow(startingNode *externalapi.DomainHash, windowSize int) (blockWindow, error) { window := make(blockWindow, 0, windowSize) - windowHashes, err := dm.dagTraversalManager.BlueWindow(startingNode, windowSize) + windowHashes, err := dm.dagTraversalManager.BlockWindow(startingNode, windowSize) if err != nil { return nil, err } diff --git a/domain/consensus/processes/difficultymanager/difficultymanager.go b/domain/consensus/processes/difficultymanager/difficultymanager.go index 15a6c33ca..003c85969 100644 --- a/domain/consensus/processes/difficultymanager/difficultymanager.go +++ b/domain/consensus/processes/difficultymanager/difficultymanager.go @@ -1,10 +1,11 @@ package difficultymanager import ( - "github.com/kaspanet/kaspad/util/difficulty" "math/big" "time" + "github.com/kaspanet/kaspad/util/difficulty" + "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" ) @@ -88,7 +89,7 @@ func (dm *difficultyManager) RequiredDifficulty(blockHash *externalapi.DomainHas } // Fetch window of dag.difficultyAdjustmentWindowSize + 1 so we can have dag.difficultyAdjustmentWindowSize block intervals - targetsWindow, err := dm.blueBlockWindow(bluestParent, dm.difficultyAdjustmentWindowSize+1) + targetsWindow, err := dm.blockWindow(bluestParent, dm.difficultyAdjustmentWindowSize+1) if err != nil { return 0, err } diff --git a/domain/consensus/processes/pastmediantimemanager/pastmediantimemanager.go b/domain/consensus/processes/pastmediantimemanager/pastmediantimemanager.go index a8b3e94bf..4fdd8ee71 100644 --- a/domain/consensus/processes/pastmediantimemanager/pastmediantimemanager.go +++ b/domain/consensus/processes/pastmediantimemanager/pastmediantimemanager.go @@ -57,7 +57,7 @@ func (pmtm *pastMedianTimeManager) PastMedianTime(blockHash *externalapi.DomainH return header.TimeInMilliseconds(), nil } - window, err := pmtm.dagTraversalManager.BlueWindow(selectedParentHash, 2*pmtm.timestampDeviationTolerance-1) + window, err := pmtm.dagTraversalManager.BlockWindow(selectedParentHash, 2*pmtm.timestampDeviationTolerance-1) if err != nil { return 0, err } diff --git a/domain/consensus/processes/pastmediantimemanager/pastmediantimemanager_test.go b/domain/consensus/processes/pastmediantimemanager/pastmediantimemanager_test.go new file mode 100644 index 000000000..7227e2270 --- /dev/null +++ b/domain/consensus/processes/pastmediantimemanager/pastmediantimemanager_test.go @@ -0,0 +1,83 @@ +package pastmediantimemanager_test + +import ( + "testing" + + "github.com/kaspanet/kaspad/domain/consensus" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" + "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" + "github.com/kaspanet/kaspad/domain/dagconfig" +) + +func TestPastMedianTime(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + factory := consensus.NewFactory() + tc, tearDown, err := factory.NewTestConsensus(params, false, "TestUpdateReindexRoot") + if err != nil { + t.Fatalf("NewTestConsensus: %s", err) + } + defer tearDown(false) + + numBlocks := uint32(300) + blockHashes := make([]*externalapi.DomainHash, numBlocks) + blockHashes[0] = params.GenesisHash + blockTime := params.GenesisBlock.Header.TimeInMilliseconds() + for i := uint32(1); i < numBlocks; i++ { + blockTime += 1000 + block, _, err := tc.BuildBlockWithParents([]*externalapi.DomainHash{blockHashes[i-1]}, nil, nil) + if err != nil { + t.Fatalf("BuildBlockWithParents: %s", err) + } + + newHeader := block.Header.ToMutable() + newHeader.SetTimeInMilliseconds(blockTime) + block.Header = newHeader.ToImmutable() + _, err = tc.ValidateAndInsertBlock(block) + if err != nil { + t.Fatalf("ValidateAndInsertBlock: %+v", err) + } + + blockHashes[i] = consensushashing.BlockHash(block) + } + + tests := []struct { + blockNumber uint32 + expectedMillisecondsSinceGenesis int64 + }{ + { + blockNumber: 263, + expectedMillisecondsSinceGenesis: 130000, + }, + { + blockNumber: 271, + expectedMillisecondsSinceGenesis: 138000, + }, + { + blockNumber: 241, + expectedMillisecondsSinceGenesis: 108000, + }, + { + blockNumber: 5, + expectedMillisecondsSinceGenesis: 0, + }, + } + + for _, test := range tests { + pastMedianTime, err := tc.PastMedianTimeManager().PastMedianTime(blockHashes[test.blockNumber]) + if err != nil { + t.Fatalf("PastMedianTime: %s", err) + } + + millisecondsSinceGenesis := pastMedianTime - + params.GenesisBlock.Header.TimeInMilliseconds() + + if millisecondsSinceGenesis != test.expectedMillisecondsSinceGenesis { + t.Errorf("TestCalcPastMedianTime: expected past median time of block %v to be %v milliseconds "+ + "from genesis but got %v", + test.blockNumber, test.expectedMillisecondsSinceGenesis, millisecondsSinceGenesis) + } + } + }) + +} diff --git a/domain/consensus/processes/syncmanager/antipast.go b/domain/consensus/processes/syncmanager/antipast.go index e6cce28e3..75ee8fcdd 100644 --- a/domain/consensus/processes/syncmanager/antipast.go +++ b/domain/consensus/processes/syncmanager/antipast.go @@ -1,14 +1,15 @@ package syncmanager import ( + "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" - "github.com/kaspanet/kaspad/domain/consensus/utils/hashset" "github.com/pkg/errors" ) // antiPastHashesBetween returns the hashes of the blocks between the // lowHash's antiPast and highHash's antiPast, or up to // `maxBlueScoreDifference`, if non-zero. +// The result excludes lowHash and includes highHash. If lowHash == highHash, returns nothing. func (sm *syncManager) antiPastHashesBetween(lowHash, highHash *externalapi.DomainHash, maxBlueScoreDifference uint64) ([]*externalapi.DomainHash, error) { @@ -17,19 +18,10 @@ func (sm *syncManager) antiPastHashesBetween(lowHash, highHash *externalapi.Doma // highHash's selectedParentChain. // We keep originalLowHash to filter out blocks in it's past later down the road originalLowHash := lowHash - for { - isInSelectedParentChain, err := sm.dagTopologyManager.IsInSelectedParentChainOf(lowHash, highHash) - if err != nil { - return nil, err - } - if isInSelectedParentChain { - break - } - lowBlockGHOSTDAGData, err := sm.ghostdagDataStore.Get(sm.databaseContext, lowHash) - if err != nil { - return nil, err - } - lowHash = lowBlockGHOSTDAGData.SelectedParent() + var err error + lowHash, err = sm.findLowHashInHighHashSelectedParentChain(lowHash, highHash) + if err != nil { + return nil, err } lowBlockGHOSTDAGData, err := sm.ghostdagDataStore.Get(sm.databaseContext, lowHash) @@ -55,78 +47,138 @@ func (sm *syncManager) antiPastHashesBetween(lowHash, highHash *externalapi.Doma // Using blueScore as an approximation is considered to be // fairly accurate because we presume that most DAG blocks are // blue. - iterator, err := sm.dagTraversalManager.SelectedChildIterator(highHash, lowHash) + highHash, err = sm.findHighHashAccordingToMaxBlueScoreDifference(lowHash, highHash, maxBlueScoreDifference, highBlockGHOSTDAGData, lowBlockGHOSTDAGData) if err != nil { return nil, err } - for ok := iterator.First(); ok; ok = iterator.Next() { - highHash, err = iterator.Get() - if err != nil { - return nil, err - } - highBlockGHOSTDAGData, err = sm.ghostdagDataStore.Get(sm.databaseContext, highHash) - if err != nil { - return nil, err - } - if highBlockGHOSTDAGData.BlueScore()-lowBlockGHOSTDAGData.BlueScore()+1 > maxBlueScoreDifference { - break - } - } } - // Collect every node in highHash's past (including itself) but - // NOT in the lowHash's past (excluding itself) into an up-heap - // (a heap sorted by blueScore from lowest to greatest). - visited := hashset.New() - hashesUpHeap := sm.dagTraversalManager.NewUpHeap() - queue := sm.dagTraversalManager.NewDownHeap() - err = queue.Push(highHash) + // Collect all hashes by concatenating the merge-sets of all blocks between highHash and lowHash + blockHashes := []*externalapi.DomainHash{} + iterator, err := sm.dagTraversalManager.SelectedChildIterator(highHash, lowHash) if err != nil { return nil, err } - for queue.Len() > 0 { - current := queue.Pop() - if visited.Contains(current) { - continue - } - visited.Add(current) - var isCurrentAncestorOfLowHash bool - if current == lowHash { - isCurrentAncestorOfLowHash = false - } else { - var err error - isCurrentAncestorOfLowHash, err = sm.dagTopologyManager.IsAncestorOf(current, lowHash) - if err != nil { - return nil, err - } - } - if isCurrentAncestorOfLowHash { - continue - } - // Push current to hashesUpHeap if it's not in the past of originalLowHash - isInPastOfOriginalLowHash, err := sm.dagTopologyManager.IsAncestorOf(current, originalLowHash) + for ok := iterator.First(); ok; ok = iterator.Next() { + current, err := iterator.Get() if err != nil { return nil, err } - if !isInPastOfOriginalLowHash { - err = hashesUpHeap.Push(current) - if err != nil { - return nil, err - } - } - parents, err := sm.dagTopologyManager.Parents(current) + // Both blue and red merge sets are topologically sorted, but not the concatenation of the two. + // We require the blocks to be topologically sorted. In addition, for optimal performance, + // we want the selectedParent to be first. + // Since the rest of the merge set is in the anticone of selectedParent, it's position in the list does not + // matter, even though it's blue score is the highest, we can arbitrarily decide it comes first. + // Therefore we first append the selectedParent, then the rest of blocks in ghostdag order. + sortedMergeSet, err := sm.getSortedMergeSet(current) if err != nil { return nil, err } - for _, parent := range parents { - err := queue.Push(parent) + + // append to blockHashes all blocks in sortedMergeSet which are not in the past of originalLowHash + for _, blockHash := range sortedMergeSet { + isInPastOfOriginalLowHash, err := sm.dagTopologyManager.IsAncestorOf(blockHash, originalLowHash) if err != nil { return nil, err } + if isInPastOfOriginalLowHash { + continue + } + blockHashes = append(blockHashes, blockHash) } } - return hashesUpHeap.ToSlice(), nil + // The process above doesn't return highHash, so include it explicitly, unless highHash == lowHash + if !lowHash.Equal(highHash) { + blockHashes = append(blockHashes, highHash) + } + + return blockHashes, nil +} + +func (sm *syncManager) getSortedMergeSet(current *externalapi.DomainHash) ([]*externalapi.DomainHash, error) { + currentGhostdagData, err := sm.ghostdagDataStore.Get(sm.databaseContext, current) + if err != nil { + return nil, err + } + + blueMergeSet := currentGhostdagData.MergeSetBlues() + redMergeSet := currentGhostdagData.MergeSetReds() + sortedMergeSet := make([]*externalapi.DomainHash, 0, len(blueMergeSet)+len(redMergeSet)) + selectedParent, blueMergeSet := blueMergeSet[0], blueMergeSet[1:] + sortedMergeSet = append(sortedMergeSet, selectedParent) + i, j := 0, 0 + for i < len(blueMergeSet) && j < len(redMergeSet) { + currentBlue := blueMergeSet[i] + currentBlueGhostdagData, err := sm.ghostdagDataStore.Get(sm.databaseContext, currentBlue) + if err != nil { + return nil, err + } + currentRed := redMergeSet[j] + currentRedGhostdagData, err := sm.ghostdagDataStore.Get(sm.databaseContext, currentRed) + if err != nil { + return nil, err + } + if sm.ghostdagManager.Less(currentBlue, currentBlueGhostdagData, currentRed, currentRedGhostdagData) { + sortedMergeSet = append(sortedMergeSet, currentBlue) + i++ + } else { + sortedMergeSet = append(sortedMergeSet, currentRed) + j++ + } + } + sortedMergeSet = append(sortedMergeSet, blueMergeSet[i:]...) + sortedMergeSet = append(sortedMergeSet, redMergeSet[j:]...) + + return sortedMergeSet, nil +} + +func (sm *syncManager) findHighHashAccordingToMaxBlueScoreDifference(lowHash *externalapi.DomainHash, + highHash *externalapi.DomainHash, maxBlueScoreDifference uint64, highBlockGHOSTDAGData *model.BlockGHOSTDAGData, + lowBlockGHOSTDAGData *model.BlockGHOSTDAGData) (*externalapi.DomainHash, error) { + + if highBlockGHOSTDAGData.BlueScore()-lowBlockGHOSTDAGData.BlueScore() <= maxBlueScoreDifference { + return highHash, nil + } + + iterator, err := sm.dagTraversalManager.SelectedChildIterator(highHash, lowHash) + if err != nil { + return nil, err + } + for ok := iterator.First(); ok; ok = iterator.Next() { + highHashCandidate, err := iterator.Get() + if err != nil { + return nil, err + } + highBlockGHOSTDAGData, err = sm.ghostdagDataStore.Get(sm.databaseContext, highHashCandidate) + if err != nil { + return nil, err + } + if highBlockGHOSTDAGData.BlueScore()-lowBlockGHOSTDAGData.BlueScore() > maxBlueScoreDifference { + break + } + highHash = highHashCandidate + } + return highHash, nil +} + +func (sm *syncManager) findLowHashInHighHashSelectedParentChain( + lowHash *externalapi.DomainHash, highHash *externalapi.DomainHash) (*externalapi.DomainHash, error) { + for { + isInSelectedParentChain, err := sm.dagTopologyManager.IsInSelectedParentChainOf(lowHash, highHash) + if err != nil { + return nil, err + } + if isInSelectedParentChain { + break + } + lowBlockGHOSTDAGData, err := sm.ghostdagDataStore.Get(sm.databaseContext, lowHash) + if err != nil { + return nil, err + } + lowHash = lowBlockGHOSTDAGData.SelectedParent() + } + return lowHash, nil } func (sm *syncManager) missingBlockBodyHashes(highHash *externalapi.DomainHash) ([]*externalapi.DomainHash, error) { diff --git a/domain/consensus/processes/syncmanager/syncmanager_test.go b/domain/consensus/processes/syncmanager/syncmanager_test.go index d01adaa1b..41c6dd032 100644 --- a/domain/consensus/processes/syncmanager/syncmanager_test.go +++ b/domain/consensus/processes/syncmanager/syncmanager_test.go @@ -1,14 +1,15 @@ package syncmanager_test import ( - "github.com/kaspanet/kaspad/domain/consensus" - "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" - "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" - "github.com/kaspanet/kaspad/domain/dagconfig" "math" "reflect" "sort" "testing" + + "github.com/kaspanet/kaspad/domain/consensus" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" + "github.com/kaspanet/kaspad/domain/dagconfig" ) func TestSyncManager_GetHashesBetween(t *testing.T) { @@ -20,27 +21,40 @@ func TestSyncManager_GetHashesBetween(t *testing.T) { } defer teardown(false) - upBfsOrder := make([]*externalapi.DomainHash, 0, 30) - selectedParent := params.GenesisHash - upBfsOrder = append(upBfsOrder, selectedParent) + // Create a DAG with the following structure: + // merging block + // / | \ + // split1 split2 split3 + // \ | / + // merging block + // / | \ + // split1 split2 split3 + // \ | / + // etc. + expectedOrder := make([]*externalapi.DomainHash, 0, 40) + mergingBlock := params.GenesisHash for i := 0; i < 10; i++ { - parents := make([]*externalapi.DomainHash, 0, 3) - for j := 0; j < 4; j++ { - blockHash, _, err := tc.AddBlock([]*externalapi.DomainHash{selectedParent}, nil, nil) + splitBlocks := make([]*externalapi.DomainHash, 0, 3) + for j := 0; j < 3; j++ { + splitBlock, _, err := tc.AddBlock([]*externalapi.DomainHash{mergingBlock}, nil, nil) if err != nil { t.Fatalf("Failed adding block: %v", err) } - parents = append(parents, blockHash) - upBfsOrder = append(upBfsOrder, blockHash) + splitBlocks = append(splitBlocks, splitBlock) } - selectedParent, _, err = tc.AddBlock(parents, nil, nil) + sort.Sort(sort.Reverse(testutils.NewTestGhostDAGSorter(splitBlocks, tc, t))) + restOfSplitBlocks, selectedParent := splitBlocks[:len(splitBlocks)-1], splitBlocks[len(splitBlocks)-1] + expectedOrder = append(expectedOrder, selectedParent) + expectedOrder = append(expectedOrder, restOfSplitBlocks...) + + mergingBlock, _, err = tc.AddBlock(splitBlocks, nil, nil) if err != nil { t.Fatalf("Failed adding block: %v", err) } - upBfsOrder = append(upBfsOrder, selectedParent) + expectedOrder = append(expectedOrder, mergingBlock) } - for i, blockHash := range upBfsOrder { + for i, blockHash := range expectedOrder { empty, err := tc.SyncManager().GetHashesBetween(blockHash, blockHash, math.MaxUint64) if err != nil { t.Fatalf("TestSyncManager_GetHashesBetween failed returning 0 hashes on the %d'th block: %v", i, err) @@ -50,15 +64,13 @@ func TestSyncManager_GetHashesBetween(t *testing.T) { } } - allHashes, err := tc.SyncManager().GetHashesBetween(upBfsOrder[0], upBfsOrder[len(upBfsOrder)-1], math.MaxUint64) + actualOrder, err := tc.SyncManager().GetHashesBetween(params.GenesisHash, expectedOrder[len(expectedOrder)-1], math.MaxUint64) if err != nil { - t.Fatalf("TestSyncManager_GetHashesBetween failed returning allHashes: %v", err) + t.Fatalf("TestSyncManager_GetHashesBetween failed returning actualOrder: %v", err) } - sort.Sort(sort.Reverse(testutils.NewTestGhostDAGSorter(upBfsOrder, tc, t))) - upBfsOrderExcludingGenesis := upBfsOrder[1:] - if !reflect.DeepEqual(allHashes, upBfsOrderExcludingGenesis) { - t.Fatalf("TestSyncManager_GetHashesBetween expected %v\n == \n%v", allHashes, upBfsOrder) + if !reflect.DeepEqual(actualOrder, expectedOrder) { + t.Fatalf("TestSyncManager_GetHashesBetween expected: \n%s\nactual:\n%s\n", expectedOrder, actualOrder) } }) } diff --git a/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go b/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go new file mode 100644 index 000000000..4b8bf8412 --- /dev/null +++ b/domain/consensus/processes/transactionvalidator/transactionvalidator_test.go @@ -0,0 +1,251 @@ +package transactionvalidator_test + +import ( + "github.com/kaspanet/go-secp256k1" + "github.com/kaspanet/kaspad/domain/consensus" + "github.com/kaspanet/kaspad/domain/consensus/ruleerrors" + "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" + "github.com/kaspanet/kaspad/domain/consensus/utils/txscript" + "github.com/kaspanet/kaspad/domain/consensus/utils/utxo" + "github.com/kaspanet/kaspad/util" + + "math/big" + + "testing" + + "github.com/kaspanet/kaspad/domain/consensus/model" + "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" + "github.com/kaspanet/kaspad/domain/consensus/utils/constants" + "github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks" + "github.com/kaspanet/kaspad/domain/dagconfig" + "github.com/pkg/errors" +) + +type mocPastMedianTimeManager struct { + pastMedianTimeForTest int64 +} + +// PastMedianTime returns the past median time for the test. +func (mdf *mocPastMedianTimeManager) PastMedianTime(*externalapi.DomainHash) (int64, error) { + return mdf.pastMedianTimeForTest, nil +} + +func TestValidateTransactionInContextAndPopulateMassAndFee(t *testing.T) { + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { + + factory := consensus.NewFactory() + pastMedianManager := &mocPastMedianTimeManager{} + factory.SetTestPastMedianTimeManager(func(int, model.DBReader, model.DAGTraversalManager, model.BlockHeaderStore, + model.GHOSTDAGDataStore) model.PastMedianTimeManager { + return pastMedianManager + }) + tc, tearDown, err := factory.NewTestConsensus(params, false, + "TestValidateTransactionInContextAndPopulateMassAndFee") + if err != nil { + t.Fatalf("Failed create a NewTestConsensus: %s", err) + } + defer tearDown(false) + + pastMedianManager.pastMedianTimeForTest = 1 + privateKey, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatalf("Failed to generate a private key: %v", err) + } + publicKey, err := privateKey.SchnorrPublicKey() + if err != nil { + t.Fatalf("Failed to generate a public key: %v", err) + } + publicKeySerialized, err := publicKey.Serialize() + if err != nil { + t.Fatalf("Failed to serialize public key: %v", err) + } + addr, err := util.NewAddressPubKeyHashFromPublicKey(publicKeySerialized[:], params.Prefix) + if err != nil { + t.Fatalf("Failed to generate p2pkh address: %v", err) + } + scriptPublicKey, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("PayToAddrScript: unexpected error: %v", err) + } + prevOutTxID := &externalapi.DomainTransactionID{} + prevOutPoint := externalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1} + + txInput := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: []byte{}, + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + 100_000_000, // 1 KAS + scriptPublicKey, + true, + uint64(5)), + } + txInputWithMaxSequence := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: []byte{}, + Sequence: constants.SequenceLockTimeIsSeconds, + UTXOEntry: utxo.NewUTXOEntry( + 100000000, // 1 KAS + scriptPublicKey, + true, + uint64(5)), + } + txInputWithLargeAmount := externalapi.DomainTransactionInput{ + PreviousOutpoint: prevOutPoint, + SignatureScript: []byte{}, + Sequence: constants.MaxTxInSequenceNum, + UTXOEntry: utxo.NewUTXOEntry( + constants.MaxSompi, + scriptPublicKey, + true, + uint64(5)), + } + + txOut := externalapi.DomainTransactionOutput{ + Value: 100000000, // 1 KAS + ScriptPublicKey: scriptPublicKey, + } + txOutBigValue := externalapi.DomainTransactionOutput{ + Value: 200_000_000, // 2 KAS + ScriptPublicKey: scriptPublicKey, + } + + validTx := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInputWithMaxSequence}, + Outputs: []*externalapi.DomainTransactionOutput{&txOut}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + txWithImmatureCoinbase := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{&txOut}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + txWithLargeAmount := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput, &txInputWithLargeAmount}, + Outputs: []*externalapi.DomainTransactionOutput{&txOut}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + txWithBigValue := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{&txOutBigValue}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + txWithInvalidSignature := externalapi.DomainTransaction{ + Version: constants.MaxTransactionVersion, + Inputs: []*externalapi.DomainTransactionInput{&txInput}, + Outputs: []*externalapi.DomainTransactionOutput{&txOut}, + SubnetworkID: subnetworks.SubnetworkIDRegistry, + Gas: 0, + LockTime: 0} + + for i, input := range validTx.Inputs { + signatureScript, err := txscript.SignatureScript(&validTx, i, scriptPublicKey, txscript.SigHashAll, privateKey) + if err != nil { + t.Fatalf("Failed to create a sigScript: %v", err) + } + input.SignatureScript = signatureScript + } + + povBlockHash := externalapi.NewDomainHashFromByteArray(&[32]byte{0x01}) + genesisHash := params.GenesisHash + tc.GHOSTDAGDataStore().Stage(model.VirtualBlockHash, model.NewBlockGHOSTDAGData( + params.BlockCoinbaseMaturity+txInput.UTXOEntry.BlockBlueScore(), + new(big.Int), + genesisHash, + make([]*externalapi.DomainHash, 1000), + make([]*externalapi.DomainHash, 1), + nil)) + tc.GHOSTDAGDataStore().Stage(povBlockHash, model.NewBlockGHOSTDAGData( + 10, + new(big.Int), + genesisHash, + make([]*externalapi.DomainHash, 1000), + make([]*externalapi.DomainHash, 1), + nil)) + + tests := []struct { + name string + tx *externalapi.DomainTransaction + povBlockHash *externalapi.DomainHash + selectedParentMedianTime int64 + isValid bool + expectedError error + }{ + { + name: "Valid transaction", + tx: &validTx, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: 1, + isValid: true, + expectedError: nil, + }, + { // The calculated block coinbase maturity is smaller than the minimum expected blockCoinbaseMaturity. + // The povBlockHash blue score is 10 and the UTXO blue score is 5, hence the The subtraction between + // them will yield a smaller result than the required CoinbaseMaturity (currently set to 100). + name: "checkTransactionCoinbaseMaturity", + tx: &txWithImmatureCoinbase, + povBlockHash: povBlockHash, + selectedParentMedianTime: 1, + isValid: false, + expectedError: ruleerrors.ErrImmatureSpend, + }, + { // The total inputs amount is bigger than the allowed maximum (constants.MaxSompi) + name: "checkTransactionInputAmounts", + tx: &txWithLargeAmount, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: 1, + isValid: false, + expectedError: ruleerrors.ErrBadTxOutValue, + }, + { // The total SompiIn (sum of inputs amount) is smaller than the total SompiOut (sum of outputs value) and hence invalid. + name: "checkTransactionOutputAmounts", + tx: &txWithBigValue, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: 1, + isValid: false, + expectedError: ruleerrors.ErrSpendTooHigh, + }, + { // the selectedParentMedianTime is negative and hence invalid. + name: "checkTransactionSequenceLock", + tx: &validTx, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: -1, + isValid: false, + expectedError: ruleerrors.ErrUnfinalizedTx, + }, + { // The SignatureScript (in the txInput) is empty and hence invalid. + name: "checkTransactionScripts", + tx: &txWithInvalidSignature, + povBlockHash: model.VirtualBlockHash, + selectedParentMedianTime: 1, + isValid: false, + expectedError: ruleerrors.ErrScriptValidation, + }, + } + + for _, test := range tests { + err := tc.TransactionValidator().ValidateTransactionInContextAndPopulateMassAndFee(test.tx, + test.povBlockHash, test.selectedParentMedianTime) + + if test.isValid { + if err != nil { + t.Fatalf("Unexpected error on TestValidateTransactionInContextAndPopulateMassAndFee"+ + " on test %v: %v", test.name, err) + } + } else { + if err == nil || !errors.Is(err, test.expectedError) { + t.Fatalf("TestValidateTransactionInContextAndPopulateMassAndFee: test %v:"+ + " Unexpected error: Expected to: %v, but got : %v", test.name, test.expectedError, err) + } + } + } + }) +} diff --git a/domain/consensus/test_consensus.go b/domain/consensus/test_consensus.go index c0879e630..91a52b529 100644 --- a/domain/consensus/test_consensus.go +++ b/domain/consensus/test_consensus.go @@ -2,6 +2,8 @@ package consensus import ( "encoding/json" + "io" + "github.com/kaspanet/kaspad/domain/consensus/model" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" "github.com/kaspanet/kaspad/domain/consensus/model/testapi" @@ -9,7 +11,6 @@ import ( "github.com/kaspanet/kaspad/domain/dagconfig" "github.com/kaspanet/kaspad/infrastructure/db/database" "github.com/pkg/errors" - "io" ) type testConsensus struct { diff --git a/domain/consensus/test_consensus_render_to_dot.go b/domain/consensus/test_consensus_render_to_dot.go new file mode 100644 index 000000000..16f078f8d --- /dev/null +++ b/domain/consensus/test_consensus_render_to_dot.go @@ -0,0 +1,79 @@ +package consensus + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os/exec" + "strings" +) + +// RenderDAGToDot is a helper function for debugging tests. +// It requires graphviz installed. +func (tc *testConsensus) RenderDAGToDot(filename string) error { + dotScript, _ := tc.convertToDot() + return renderDotScript(dotScript, filename) +} + +func (tc *testConsensus) convertToDot() (string, error) { + var dotScriptBuilder strings.Builder + dotScriptBuilder.WriteString("digraph {\n\trankdir = TB; \n") + + edges := []string{} + + blocksIterator, err := tc.blockStore.AllBlockHashesIterator(tc.databaseContext) + if err != nil { + return "", err + } + + for ok := blocksIterator.First(); ok; ok = blocksIterator.Next() { + hash, err := blocksIterator.Get() + if err != nil { + return "", err + } + dotScriptBuilder.WriteString(fmt.Sprintf("\t\"%s\";\n", hash)) + + parents, err := tc.dagTopologyManager.Parents(hash) + if err != nil { + return "", err + } + + for _, parentHash := range parents { + edges = append(edges, fmt.Sprintf("\t\"%s\" -> \"%s\";", hash, parentHash)) + } + } + + dotScriptBuilder.WriteString("\n") + + dotScriptBuilder.WriteString(strings.Join(edges, "\n")) + + dotScriptBuilder.WriteString("\n}") + + return dotScriptBuilder.String(), nil +} + +func renderDotScript(dotScript string, filename string) error { + command := exec.Command("dot", "-Tsvg") + stdin, err := command.StdinPipe() + if err != nil { + return fmt.Errorf("Error creating stdin pipe: %s", err) + } + spawn("renderDotScript", func() { + defer stdin.Close() + + _, err = io.WriteString(stdin, dotScript) + if err != nil { + panic(fmt.Errorf("Error writing dotScript into stdin pipe: %s", err)) + } + }) + + var stderr bytes.Buffer + command.Stderr = &stderr + svg, err := command.Output() + if err != nil { + return fmt.Errorf("Error getting output of dot: %s\nstderr:\n%s", err, stderr.String()) + } + + return ioutil.WriteFile(filename, svg, 0600) +} diff --git a/domain/miningmanager/miningmanager_test.go b/domain/miningmanager/miningmanager_test.go index 58c26fbcd..e8179ca0c 100644 --- a/domain/miningmanager/miningmanager_test.go +++ b/domain/miningmanager/miningmanager_test.go @@ -1,130 +1,131 @@ package miningmanager_test import ( - "bytes" - "github.com/kaspanet/kaspad/app/appmessage" + //"bytes" + //"github.com/kaspanet/kaspad/app/appmessage" + "github.com/kaspanet/kaspad/domain" "github.com/kaspanet/kaspad/domain/consensus" "github.com/kaspanet/kaspad/domain/consensus/model/externalapi" - "github.com/kaspanet/kaspad/domain/consensus/utils/coinbase" - "github.com/kaspanet/kaspad/domain/consensus/utils/consensusserialization" + "github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" + + //"github.com/kaspanet/kaspad/domain/consensus/ruleerrors" + //"github.com/kaspanet/kaspad/domain/consensus/utils/coinbase" + //"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing" + //"github.com/kaspanet/kaspad/domain/consensus/utils/consensusserialization" "github.com/kaspanet/kaspad/domain/consensus/utils/constants" - "github.com/kaspanet/kaspad/domain/consensus/utils/hashes" + //"github.com/kaspanet/kaspad/domain/consensus/utils/hashes" "github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks" + "github.com/kaspanet/kaspad/domain/consensus/utils/testutils" "github.com/kaspanet/kaspad/domain/consensus/utils/txscript" "github.com/kaspanet/kaspad/domain/dagconfig" - "github.com/kaspanet/kaspad/domain/miningmanager" - infrastructuredatabase "github.com/kaspanet/kaspad/infrastructure/db/database" - "github.com/kaspanet/kaspad/infrastructure/db/database/ldb" + //"github.com/kaspanet/kaspad/domain/miningmanager" + //infrastructuredatabase "github.com/kaspanet/kaspad/infrastructure/db/database" + //"github.com/kaspanet/kaspad/infrastructure/db/database/ldb" "github.com/kaspanet/kaspad/util" - "github.com/pkg/errors" - "github.com/syndtr/goleveldb/leveldb/opt" - "io/ioutil" - "os" - "path/filepath" + //"github.com/pkg/errors" + //"github.com/syndtr/goleveldb/leveldb/opt" + //"io/ioutil" + //"os" + //"path/filepath" "testing" ) -func setupDBForTest(dbName string) (infrastructuredatabase.Database, func(), error) { - var err error - tmpDir, err := ioutil.TempDir("", "setupDBManager") - if err != nil { - return nil, nil, errors.Errorf("error creating temp dir: %s", err) - } - - dbPath := filepath.Join(tmpDir, dbName) - _ = os.RemoveAll(dbPath) - db, err := ldb.NewLevelDB(dbPath) - if err != nil { - return nil, nil, err - } - - originalLDBOptions := ldb.Options - ldb.Options = func() *opt.Options { - return nil - } - - teardown := func() { - db.Close() - ldb.Options = originalLDBOptions - os.RemoveAll(dbPath) - } - - return db, teardown, err -} +//func setupDBForTest(dbName string) (infrastructuredatabase.Database, func(), error) { +// var err error +// tmpDir, err := ioutil.TempDir("", "setupDBManager") +// if err != nil { +// return nil, nil, errors.Errorf("error creating temp dir: %s", err) +// } +// +// dbPath := filepath.Join(tmpDir, dbName) +// _ = os.RemoveAll(dbPath) +// db, err := ldb.NewLevelDB(dbPath) +// if err != nil { +// return nil, nil, err +// } +// +// originalLDBOptions := ldb.Options +// ldb.Options = func() *opt.Options { +// return nil +// } +// +// teardown := func() { +// db.Close() +// ldb.Options = originalLDBOptions +// os.RemoveAll(dbPath) +// } +// +// return db, teardown, err +//} func createCoinbaseTransaction(t *testing.T, scriptPublicKey []byte, value uint64) *externalapi.DomainTransaction { dummyTxOut := externalapi.DomainTransactionOutput{ Value: value, - ScriptPublicKey: scriptPublicKey, - } - payload, err := coinbase.SerializeCoinbasePayload(1, &externalapi.DomainCoinbaseData{ - ScriptPublicKey:scriptPublicKey, - }) - if err != nil { - t.Fatalf("SerializeCoinbasePayload: %v", err) + ScriptPublicKey: nil, } - payloadHash := hashes.HashData(payload) transaction := &externalapi.DomainTransaction{ - Version: constants.TransactionVersion, + Version: constants.MaxTransactionVersion, Inputs: []*externalapi.DomainTransactionInput{}, Outputs: []*externalapi.DomainTransactionOutput{&dummyTxOut}, LockTime: 0, SubnetworkID: subnetworks.SubnetworkIDCoinbase, - Gas: 0, - PayloadHash: *payloadHash, - Payload: payload, - } - - return transaction -} - -func createTransaction(inputs []*externalapi.DomainTransactionInput, scriptPublicKey []byte, value uint64) *externalapi.DomainTransaction { - dummyTxOut := externalapi.DomainTransactionOutput{ - Value: value, - ScriptPublicKey: scriptPublicKey, - } - - transaction := &externalapi.DomainTransaction{ - Version: constants.TransactionVersion, - Inputs: inputs, - Outputs: []*externalapi.DomainTransactionOutput{&dummyTxOut}, - LockTime: 0, - SubnetworkID: subnetworks.SubnetworkIDNative, - Gas: 0, } return transaction } +// +//func createTransaction(inputs []*externalapi.DomainTransactionInput, scriptPublicKey []byte, value uint64) *externalapi.DomainTransaction { +// dummyTxOut := externalapi.DomainTransactionOutput{ +// Value: value, +// ScriptPublicKey: scriptPublicKey, +// } +// +// transaction := &externalapi.DomainTransaction{ +// Version: constants.MaxTransactionVersion, +// Inputs: inputs, +// Outputs: []*externalapi.DomainTransactionOutput{&dummyTxOut}, +// LockTime: 0, +// SubnetworkID: subnetworks.SubnetworkIDNative, +// Gas: 0, +// } +// +// return transaction +//} func TestMiningManager(t *testing.T) { - dagParams := &dagconfig.SimnetParams - consensusFactory := consensus.NewFactory() - miningManagerFactory := miningmanager.NewFactory() - db, teardownFunc, err := setupDBForTest(t.Name()) - if err != nil { - t.Fatalf("Failed to setup db: %v", err) - } - defer teardownFunc() + testutils.ForAllNets(t, true, func(t *testing.T, params *dagconfig.Params) { - miningAddrHash := [20]byte{0x99} - miningAddr, err := util.NewAddressPubKeyHash(miningAddrHash[:], util.Bech32PrefixKaspaTest) - if err != nil { - t.Fatalf("NewAddressPubKeyHash: unexpected error: %v", err) - } - scriptPublicKey, err := txscript.PayToAddrScript(miningAddr) - - t.Run("Spending all transactions", func(t *testing.T) { - consensusInstance, err := consensusFactory.NewConsensus(dagParams, db) + factory := consensus.NewFactory() + tc, teardown, err := factory.NewTestConsensus(params, false, "TestBlockSize") if err != nil { - t.Fatalf("NewConsensus: %v", err) + t.Fatalf("Error setting up tc: %+v", err) } + defer teardown(false) + tc.Database() + + miningAddrHash := [20]byte{0x99} + miningAddr, err := util.NewAddressPubKeyHash(miningAddrHash[:], util.Bech32PrefixKaspaTest) + if err != nil { + t.Fatalf("NewAddressPubKeyHash: unexpected error: %v", err) + } + scriptPublicKey, err := txscript.PayToAddrScript(miningAddr) + isArchivalNode := false + domain, err := domain.New(params, tc.Database(), isArchivalNode) + if err != nil { + t.Fatalf("err") + } + //consensusInstance, err := consensusFactory.NewConsensus(dagParams, db) + //if err != nil { + // t.Fatalf("NewConsensus: %v", err) + //} // Insert 10 transactions - miningManager := miningManagerFactory.NewMiningManager(consensusInstance, constants.MaxMassAcceptedByBlock) + miningManager := domain.MiningManager() + //miningManager := miningManagerFactory.NewMiningManager(consensusInstance, constants.MaxMassAcceptedByBlock) transactions := make([]*externalapi.DomainTransaction, 10) for i := range transactions { - transaction := createCoinbaseTransaction(t, scriptPublicKey, uint64(100000000+i)) + transaction := createCoinbaseTransaction(t, scriptPublicKey.Script, uint64(100000000+i)) transactions[i] = transaction err = miningManager.ValidateAndInsertTransaction(transaction, true) if err != nil { @@ -134,9 +135,12 @@ func TestMiningManager(t *testing.T) { // Spending 10 transactions miningManager.HandleNewBlockTransactions(transactions) - block := miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ + block, err := miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ ScriptPublicKey: scriptPublicKey, }) + if err != nil { + // todo + } if block == nil { t.Fatalf("GetBlockTemplate: failed building block") } @@ -144,122 +148,179 @@ func TestMiningManager(t *testing.T) { // Check 10 transactions are not exist for _, tx2 := range transactions { for _, tx1 := range block.Transactions { - if consensusserialization.TransactionID(tx1) == consensusserialization.TransactionID(tx2) { + if consensushashing.TransactionID(tx1) == consensushashing.TransactionID(tx2) { t.Fatalf("Spent tranasaction is still exisit") } } } - }) - t.Run("Spending some transactions", func(t *testing.T) { - consensusInstance, err := consensusFactory.NewConsensus(dagParams, db) - if err != nil { - t.Fatalf("NewConsensus: %v", err) - } - - // Insert 10 transactions - miningManager := miningManagerFactory.NewMiningManager(consensusInstance, constants.MaxMassAcceptedByBlock) - transactions := make([]*externalapi.DomainTransaction, 10) - for i := range transactions { - transaction := createCoinbaseTransaction(t, scriptPublicKey, uint64(100000000+i)) - transactions[i] = transaction - err = miningManager.ValidateAndInsertTransaction(transaction, true) - if err != nil { - t.Fatalf("ValidateAndInsertTransaction: unexpected error: %v", err) - } - } - - // Spending first 5 transactions - miningManager.HandleNewBlockTransactions(transactions[0:5]) - block := miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ - ScriptPublicKey: scriptPublicKey, - }) - if block == nil { - t.Fatalf("GetBlockTemplate: failed building block") - } - - // Check first 5 transactions are not exist - for _, tx2 := range transactions[0:5] { - for _, tx1 := range block.Transactions { - if consensusserialization.TransactionID(tx1) == consensusserialization.TransactionID(tx2) { - t.Fatalf("Spent tranasaction is still exisit") - } - } - } - }) - - t.Run("Spending transactions with unknown parents", func(t *testing.T) { - consensusInstance, err := consensusFactory.NewConsensus(dagParams, db) - if err != nil { - t.Fatalf("NewConsensus: %v", err) - } - - miningManager := miningManagerFactory.NewMiningManager(consensusInstance, constants.MaxMassAcceptedByBlock) - transactions := make([]*externalapi.DomainTransaction, 10) - parentTransactions := make([]*externalapi.DomainTransaction, len(transactions)) - for i := range parentTransactions { - parentTransaction := createCoinbaseTransaction(t, scriptPublicKey, uint64(100000000+i)) - parentTransactions[i] = parentTransaction - } - - // Insert transactions with unknown parents - for i := range transactions { - parentTransaction := parentTransactions[i] - txIn := externalapi.DomainTransactionInput{ - PreviousOutpoint: externalapi.DomainOutpoint{TransactionID: *consensusserialization.TransactionID(parentTransaction), Index: 1}, - SignatureScript: bytes.Repeat([]byte{0x00}, 65), - Sequence: appmessage.MaxTxInSequenceNum, - } - transaction := createTransaction([]*externalapi.DomainTransactionInput{&txIn}, scriptPublicKey, uint64(10+i)) - transactions[i] = transaction - err = miningManager.ValidateAndInsertTransaction(transaction, true) - if err != nil { - t.Fatalf("ValidateAndInsertTransaction: unexpected error: %v", err) - } - } - - // Check transactions with unknown parents - block := miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ - ScriptPublicKey: scriptPublicKey, - }) - if block == nil { - t.Fatalf("GetBlockTemplate: failed building block") - } - - for _, tx1 := range transactions { - for _, tx2 := range block.Transactions { - if consensusserialization.TransactionID(tx1) == consensusserialization.TransactionID(tx2) { - t.Fatalf("Tranasaction with unknown parents is exisit") - } - } - } - - // Add the missing parents - for _, parentTransaction := range parentTransactions { - err = miningManager.ValidateAndInsertTransaction(parentTransaction, true) - if err != nil { - t.Fatalf("ValidateAndInsertTransaction: unexpected error: %v", err) - } - } - block = miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ - ScriptPublicKey: scriptPublicKey, - }) - if block == nil { - t.Fatalf("GetBlockTemplate: failed building block") - } - - numberOfFoundTransactions := 0 - for _, tx1 := range transactions { - for _, tx2 := range block.Transactions { - if consensusserialization.TransactionID(tx1) == consensusserialization.TransactionID(tx2) { - numberOfFoundTransactions++ - break - } - } - } - - if len(transactions) != numberOfFoundTransactions{ - t.Fatalf("Not all transactions are exist, expected %v, but got %v", len(transactions), numberOfFoundTransactions) - } }) } + +// +//func TestMiningManager(t *testing.T) { +// dagParams := &dagconfig.SimnetParams +// consensusFactory := consensus.NewFactory() +// miningManagerFactory := miningmanager.NewFactory() +// db, teardownFunc, err := setupDBForTest(t.Name()) +// if err != nil { +// t.Fatalf("Failed to setup db: %v", err) +// } +// defer teardownFunc() +// +// miningAddrHash := [20]byte{0x99} +// miningAddr, err := util.NewAddressPubKeyHash(miningAddrHash[:], util.Bech32PrefixKaspaTest) +// if err != nil { +// t.Fatalf("NewAddressPubKeyHash: unexpected error: %v", err) +// } +// scriptPublicKey, err := txscript.PayToAddrScript(miningAddr) +// +// t.Run("Spending all transactions", func(t *testing.T) { +// consensusInstance, err := consensusFactory.NewConsensus(dagParams, db) +// if err != nil { +// t.Fatalf("NewConsensus: %v", err) +// } +// +// // Insert 10 transactions +// miningManager := miningManagerFactory.NewMiningManager(consensusInstance, constants.MaxMassAcceptedByBlock) +// transactions := make([]*externalapi.DomainTransaction, 10) +// for i := range transactions { +// transaction := createCoinbaseTransaction(t, scriptPublicKey, uint64(100000000+i)) +// transactions[i] = transaction +// err = miningManager.ValidateAndInsertTransaction(transaction, true) +// if err != nil { +// t.Fatalf("ValidateAndInsertTransaction: unexpected error: %v", err) +// } +// } +// +// // Spending 10 transactions +// miningManager.HandleNewBlockTransactions(transactions) +// block := miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ +// ScriptPublicKey: scriptPublicKey, +// }) +// if block == nil { +// t.Fatalf("GetBlockTemplate: failed building block") +// } +// +// // Check 10 transactions are not exist +// for _, tx2 := range transactions { +// for _, tx1 := range block.Transactions { +// if consensusserialization.TransactionID(tx1) == consensusserialization.TransactionID(tx2) { +// t.Fatalf("Spent tranasaction is still exisit") +// } +// } +// } +// }) +// +// t.Run("Spending some transactions", func(t *testing.T) { +// consensusInstance, err := consensusFactory.NewConsensus(dagParams, db) +// if err != nil { +// t.Fatalf("NewConsensus: %v", err) +// } +// +// // Insert 10 transactions +// miningManager := miningManagerFactory.NewMiningManager(consensusInstance, constants.MaxMassAcceptedByBlock) +// transactions := make([]*externalapi.DomainTransaction, 10) +// for i := range transactions { +// transaction := createCoinbaseTransaction(t, scriptPublicKey, uint64(100000000+i)) +// transactions[i] = transaction +// err = miningManager.ValidateAndInsertTransaction(transaction, true) +// if err != nil { +// t.Fatalf("ValidateAndInsertTransaction: unexpected error: %v", err) +// } +// } +// +// // Spending first 5 transactions +// miningManager.HandleNewBlockTransactions(transactions[0:5]) +// block := miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ +// ScriptPublicKey: scriptPublicKey, +// }) +// if block == nil { +// t.Fatalf("GetBlockTemplate: failed building block") +// } +// +// // Check first 5 transactions are not exist +// for _, tx2 := range transactions[0:5] { +// for _, tx1 := range block.Transactions { +// if consensusserialization.TransactionID(tx1) == consensusserialization.TransactionID(tx2) { +// t.Fatalf("Spent tranasaction is still exisit") +// } +// } +// } +// }) +// +// t.Run("Spending transactions with unknown parents", func(t *testing.T) { +// consensusInstance, err := consensusFactory.NewConsensus(dagParams, db) +// if err != nil { +// t.Fatalf("NewConsensus: %v", err) +// } +// +// miningManager := miningManagerFactory.NewMiningManager(consensusInstance, constants.MaxMassAcceptedByBlock) +// transactions := make([]*externalapi.DomainTransaction, 10) +// parentTransactions := make([]*externalapi.DomainTransaction, len(transactions)) +// for i := range parentTransactions { +// parentTransaction := createCoinbaseTransaction(t, scriptPublicKey, uint64(100000000+i)) +// parentTransactions[i] = parentTransaction +// } +// +// // Insert transactions with unknown parents +// for i := range transactions { +// parentTransaction := parentTransactions[i] +// txIn := externalapi.DomainTransactionInput{ +// PreviousOutpoint: externalapi.DomainOutpoint{TransactionID: *consensusserialization.TransactionID(parentTransaction), Index: 1}, +// SignatureScript: bytes.Repeat([]byte{0x00}, 65), +// Sequence: appmessage.MaxTxInSequenceNum, +// } +// transaction := createTransaction([]*externalapi.DomainTransactionInput{&txIn}, scriptPublicKey, uint64(10+i)) +// transactions[i] = transaction +// err = miningManager.ValidateAndInsertTransaction(transaction, true) +// if err != nil { +// t.Fatalf("ValidateAndInsertTransaction: unexpected error: %v", err) +// } +// } +// +// // Check transactions with unknown parents +// block := miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ +// ScriptPublicKey: scriptPublicKey, +// }) +// if block == nil { +// t.Fatalf("GetBlockTemplate: failed building block") +// } +// +// for _, tx1 := range transactions { +// for _, tx2 := range block.Transactions { +// if consensusserialization.TransactionID(tx1) == consensusserialization.TransactionID(tx2) { +// t.Fatalf("Tranasaction with unknown parents is exisit") +// } +// } +// } +// +// // Add the missing parents +// for _, parentTransaction := range parentTransactions { +// err = miningManager.ValidateAndInsertTransaction(parentTransaction, true) +// if err != nil { +// t.Fatalf("ValidateAndInsertTransaction: unexpected error: %v", err) +// } +// } +// block = miningManager.GetBlockTemplate(&externalapi.DomainCoinbaseData{ +// ScriptPublicKey: scriptPublicKey, +// }) +// if block == nil { +// t.Fatalf("GetBlockTemplate: failed building block") +// } +// +// numberOfFoundTransactions := 0 +// for _, tx1 := range transactions { +// for _, tx2 := range block.Transactions { +// if consensusserialization.TransactionID(tx1) == consensusserialization.TransactionID(tx2) { +// numberOfFoundTransactions++ +// break +// } +// } +// } +// +// if len(transactions) != numberOfFoundTransactions{ +// t.Fatalf("Not all transactions are exist, expected %v, but got %v", len(transactions), numberOfFoundTransactions) +// } +// }) +//} diff --git a/go.mod b/go.mod index 6c95f6940..8cc86ed03 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kaspanet/kaspad -go 1.15 +go 1.16 require ( github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd