package tests import ( "kerma/helpers" "kerma/models" "os" "testing" "github.com/docker/go/canonical/json" . "github.com/smartystreets/goconvey/convey" ) func TestState(t *testing.T) { // Prepare testDir := t.TempDir() t.Setenv("KERMA_STORE_BASE_DIR", testDir) t.Setenv("KERMA_INITIAL_PEER_LIST", "example.com:18018") var validator helpers.Validator var config helpers.Config validator.Construct() config.Construct() peerList := []models.Peer{ { Name: "example.com:18018", Active: false, }, } Convey("Given a new state object", t, func() { var state models.State Convey("When the Construct method is called", func() { state.Construct() Convey("A valid object should be created", func() { So(state.Validator, ShouldResemble, validator) So(state.Config, ShouldResemble, config) So(state.PeerList, ShouldResemble, peerList) So(state.Chain, ShouldResemble, []models.Block{*models.GetGenesisBlock()}) So(state.Transactions, ShouldResemble, map[string]*models.Transaction{}) }) }) state.Construct() Convey("When the CompleteHandshake method is called", func() { Convey("When the peer is already in the list, it should be marked as active", func() { state.CompleteHandshake("example.com:18018") So(state.PeerList[0].Active, ShouldEqual, true) }) Convey("When the peer is not in the list, it should be created and marked as active", func() { state.CompleteHandshake("valid.host:18018") So(state.PeerList[1].Name, ShouldEqual, "valid.host:18018") So(state.PeerList[1].Active, ShouldEqual, true) }) }) Convey("When the CheckForHandshake method is called", func() { Convey("When there is a finished handshake, 'true' should be returned", func() { state.PeerList[0].Active = true So(state.CheckForHandshake("example.com:18018"), ShouldEqual, true) }) Convey("When there is no finished handshake, 'false' should be returned", func() { So(state.CheckForHandshake("missing.host:18018"), ShouldEqual, false) }) }) Convey("When the FetchPeerListResponse method is called, the list of peers should be returned", func() { res := state.FetchPeerListResponse() peerListResponse := models.PeerListResponse{ Type: "peers", Peers: []string{ "example.com:18018", }, } So(res, ShouldResemble, peerListResponse) }) Convey("When the FindPeerByName method is called", func() { Convey("When the peer exists, it should be returned", func() { res := state.FindPeerByName("example.com:18018") So(res, ShouldEqual, &state.PeerList[0]) }) Convey("When the peer does not exist, nil should be returned", func() { res := state.FindPeerByName("example1.com:18018") So(res, ShouldEqual, nil) }) }) Convey("When the RemovePeerByName method is called", func() { SkipConvey("When the peer exists, it should be removed", func() { prevLen := len(state.PeerList) state.RemovePeerByName("example.com:18018") So(len(state.PeerList), ShouldEqual, prevLen-1) }) Convey("When the peer does not exist, nothing should be done", func() { prevLen := len(state.PeerList) state.RemovePeerByName("example1.com:18018") So(len(state.PeerList), ShouldEqual, prevLen) }) }) Convey("When the ParsePeerListResponse method is called", func() { Convey("With a non-empty input, all new peers should be added to the peer list", func() { peerListResponse := models.PeerListResponse{ Type: "peers", Peers: []string{ "sub.example.com:18018", "sub1.example.com:18018", }, } state.ParsePeerListResponse(&peerListResponse) So(state.FindPeerByName("sub.example.com:18018"), ShouldResemble, &models.Peer{Name: "sub.example.com:18018", Active: false}) So(state.FindPeerByName("sub1.example.com:18018"), ShouldResemble, &models.Peer{Name: "sub1.example.com:18018", Active: false}) }) Convey("With an empty input, no new peers should be added to the peer list", func() { peerListResponse := models.PeerListResponse{ Type: "peers", Peers: []string{}, } prevLen := len(state.PeerList) state.ParsePeerListResponse(&peerListResponse) So(len(state.PeerList), ShouldEqual, prevLen) }) }) Convey("When the DumpPeerListStore method is called, the peer list should be saved to the peer list store file", func() { state.DumpPeerListStore() res, oserr := os.Stat(state.Config.PeerListStore) file, _ := os.ReadFile(state.Config.PeerListStore) peerList := []string{} err := json.Unmarshal([]byte(file), &peerList) So(oserr, ShouldBeNil) So(res.Size(), ShouldNotEqual, 0) So(res.IsDir(), ShouldBeFalse) So(err, ShouldBeNil) So(len(peerList), ShouldEqual, 3) So(peerList[0], ShouldEqual, "example.com:18018") }) Convey("Given a correct block", func() { previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e" txid := "2a9458a2e75ed8bd0341b3cb2ab21015bbc13f21ea06229340a7b2b75720c4df" height := uint64(1) transaction := models.Transaction{ Type: "transaction", Height: &height, Outputs: []models.Output{ { Pubkey: "f66c7d51551d344b74e071d3b988d2bc09c3ffa82857302620d14f2469cfbf60", Value: 50000000000000, }, }, } state.Transactions[txid] = &transaction block := models.Block{ Type: "block", Txids: []string{ txid, }, Nonce: "000000000000000000000000000000000000000000000000000000009d8b60ea", Previd: &previd, Created: 1624220079, Target: "00000002af000000000000000000000000000000000000000000000000000000", Miner: "Snekel testminer", Note: "First block after genesis with CBTX", } Convey("When AppendToChain is called", func() { err := state.AppendToChain(&block) Convey("No error should be returned", func() { So(err, ShouldBeNil) }) Convey("When GetChainTip is called, the new block should be returned", func() { So(state.GetChainTip(), ShouldResemble, &block) }) Convey("When GetBlock is called, the new block should be returned", func() { So(state.GetBlock(block.GetID()), ShouldResemble, &block) }) Convey("When GetMissingTransactionsInBlock is called, an empty array and no error should be returned", func() { res, err := state.GetMissingTransactionsInBlock(&block) So(res, ShouldBeEmpty) So(err, ShouldBeNil) }) Convey("When the DumpBlockStore method is called, the blockchain should be saved to the blockchain store file", func() { state.DumpBlockStore() res, oserr := os.Stat(state.Config.BlockStore) file, _ := os.ReadFile(state.Config.BlockStore) chain := []models.Block{} err := json.Unmarshal([]byte(file), &chain) // Set UTXO of both chain blocks and Height, as they are always recalculated genesis := models.GetGenesisBlock() genesis.UTXOSet = map[string]uint64(nil) block.UTXOSet = map[string]uint64(nil) block.Height = 0 So(oserr, ShouldBeNil) So(res.Size(), ShouldNotEqual, 0) So(res.IsDir(), ShouldBeFalse) So(err, ShouldBeNil) So(len(chain), ShouldEqual, 2) So(&chain[0], ShouldResemble, genesis) So(&chain[1], ShouldResemble, &block) }) }) }) Convey("Given an incorrect block", func() { previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e" txid := "2a9458a2e75ed8bd0341b3cb2ab21015bbc13f21ea06229340a7b2b75720c4df" height := uint64(1) transaction := models.Transaction{ Type: "transaction", Height: &height, Outputs: []models.Output{ { Pubkey: "f66c7d51551d344b74e071d3b988d2bc09c3ffa82857302620d14f2469cfbf60", Value: 50000000000000, }, }, } state.Transactions[txid] = &transaction block := models.Block{ Type: "block", Txids: []string{ txid, }, Nonce: "000000000000000000000000000000000000000000000000000000009d8b60e2", Previd: &previd, Created: 1624220079, Target: "00000002af000000000000000000000000000000000000000000000000000000", Miner: "Snekel testminer", Note: "First block after genesis with CBTX", } Convey("When AppendToChain is called", func() { err := state.AppendToChain(&block) Convey("An error should be returned", func() { So(err, ShouldNotBeNil) }) }) }) Convey("Given a valid coinbase transaction", func() { height := uint64(1) transaction := models.Transaction{ Type: "transaction", Height: &height, Outputs: []models.Output{ { Pubkey: "62b7c521cd9211579cf70fd4099315643767b96711febaa5c76dc3daf27c281c", Value: 50000000000000, }, }, } Convey("When AppendTransaction is called", func() { err := state.AppendTransaction(&transaction) Convey("No error should be returned", func() { So(err, ShouldBeNil) }) Convey("GetTransaction should return the transaction", func() { So(state.GetTransaction(transaction.GetID()), ShouldResemble, &transaction) }) Convey("GetMempoolTransactionIDs should return an empty array", func() { So(state.GetMempoolTransactionIDs(), ShouldBeEmpty) }) }) }) Convey("Given an invalid coinbase transaction", func() { height := uint64(1) transaction := models.Transaction{ Type: "transaction", Height: &height, Outputs: []models.Output{}, } Convey("When AppendTransaction is called", func() { err := state.AppendTransaction(&transaction) Convey("An error should be returned", func() { So(err, ShouldNotBeNil) }) Convey("GetMempoolTransactionIDs should return an empty array", func() { So(state.GetMempoolTransactionIDs(), ShouldBeEmpty) }) }) }) Convey("Given a valid regular transaction", func() { height := uint64(1) coinbase := models.Transaction{ Type: "transaction", Height: &height, Confirmed: true, Outputs: []models.Output{ { Pubkey: "f66c7d51551d344b74e071d3b988d2bc09c3ffa82857302620d14f2469cfbf60", Value: 50000000000000, }, }, } state.Transactions[coinbase.GetID()] = &coinbase transaction := models.Transaction{ Type: "transaction", Inputs: []models.Input{ { Outpoint: models.Outpoint{ Index: 0, Txid: "2a9458a2e75ed8bd0341b3cb2ab21015bbc13f21ea06229340a7b2b75720c4df", }, Sig: "49cc4f9a1fb9d600a7debc99150e7909274c8c74edd7ca183626dfe49eb4aa21c6ff0e4c5f0dc2a328ad6b8ba10bf7169d5f42993a94bf67e13afa943b749c0b", }, }, Outputs: []models.Output{ { Pubkey: "c7c2c13afd02be7986dee0f4630df01abdbc950ea379055f1a423a6090f1b2b3", Value: 50, }, }, } Convey("When AppendTransaction is called", func() { err := state.AppendTransaction(&transaction) Convey("No error should be returned", func() { So(err, ShouldBeNil) }) Convey("GetTransaction should return the transaction", func() { So(state.GetTransaction(transaction.GetID()), ShouldResemble, &transaction) }) Convey("GetMempoolTransactionIDs should return an array with the transaction", func() { So(state.GetMempoolTransactionIDs(), ShouldNotBeEmpty) So(len(state.GetMempoolTransactionIDs()), ShouldEqual, 1) So(state.GetMempoolTransactionIDs()[0], ShouldEqual, transaction.GetID()) }) Convey("When the DumpTransactionStore method is called, the confirmed transactions should be saved to the store file", func() { state.DumpTransactionStore() res, oserr := os.Stat(state.Config.TransactionStore) file, _ := os.ReadFile(state.Config.TransactionStore) store := map[string]*models.Transaction{} err := json.Unmarshal([]byte(file), &store) // Set confirmed to false, as this is set in the parse function coinbase.Confirmed = false So(oserr, ShouldBeNil) So(res.Size(), ShouldNotEqual, 0) So(res.IsDir(), ShouldBeFalse) So(err, ShouldBeNil) So(len(store), ShouldEqual, 1) So(store[coinbase.GetID()], ShouldResemble, &coinbase) }) }) }) Convey("Given an invalid regular transaction", func() { transaction := models.Transaction{ Type: "transaction", Inputs: []models.Input{}, Outputs: []models.Output{ { Pubkey: "c7c2c13afd02be7986dee0f4630df01abdbc950ea379055f1a423a6090f1b2b3", Value: 50, }, }, } Convey("When AppendTransaction is called", func() { err := state.AppendTransaction(&transaction) Convey("An error should be returned", func() { So(err, ShouldNotBeNil) }) Convey("GetMempoolTransactionIDs should return an empty array", func() { So(state.GetMempoolTransactionIDs(), ShouldBeEmpty) }) }) }) }) }