// Package models provides the models for the KermaGo application package models import ( "crypto/ed25519" "crypto/x509" "errors" "kerma/helpers" "os" "strings" "github.com/docker/go/canonical/json" "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) /* A State is an object that contains the current state of the application. State-related fields include: - PeerList - list of discovered peers - Chain - chain of known valid blocks - Transactions - hashmap of known valid transactions - PrivateKey - ED25519 private key of the Kerma node */ type State struct { PeerList []Peer Validator helpers.Validator Config helpers.Config Chain []Block Transactions map[string]*Transaction PrivateKey ed25519.PrivateKey Logger helpers.Logger Height uint64 } /* A Peer represents a peer on the Kerma network. It has a name and an indicator of whether there was a handshake */ type Peer struct { Name string Active bool } /* A PeerListResponse represents an object with a type and a string array, containing the addresses of all known peers */ type PeerListResponse struct { Type string `json:"type" binding:"required"` Peers []string `json:"peers"` } /* Construct creates a new state object */ func (s *State) Construct() { s.PeerList = []Peer{} s.Validator.Construct() s.Config.Construct() privKey, err := s.parsePKIFile() if err != nil { s.Logger.Warn(err.Error()) s.Logger.Info("Generating new key...") _, privKey, _ = ed25519.GenerateKey(nil) } s.PrivateKey = privKey s.dumpPKIFile() s.Logger.Info("Reading known peers from dump file") dumpErr := s.parsePeerListStore() if dumpErr != nil { s.Logger.Warn(dumpErr.Error() + ", reverting to defaults...") s.parseInitialPeerList() } s.Logger.Info("Restoring previous known state") s.Chain = []Block{} s.Transactions = map[string]*Transaction{} terr := s.parseTransactionStore() if terr != nil { s.Logger.Debug(terr.Error()) } berr := s.parseBlockStore() if berr != nil { s.Logger.Debug(berr.Error()) } if len(s.Chain) == 0 { s.Chain = append(s.Chain, *GetGenesisBlock()) s.Height = 0 } } /* ============================== HANDSHAKE OPERATIONS ============================== */ /* CheckForHandshake checks whether the node has completed a handshake with the specified peer */ func (s *State) CheckForHandshake(peerName string) bool { for _, peer := range s.PeerList { if peer.Name == peerName { return peer.Active } } return false } /* CompleteHandshake completes a handshake with the specified peer */ func (s *State) CompleteHandshake(peerName string) { peer := s.FindPeerByName(peerName) if peer != nil { peer.Active = true } else { peer = &Peer{ Name: peerName, Active: true, } s.PeerList = append(s.PeerList, *peer) } } /* ====================== PEER OPERATIONS ====================== */ /* MarshalJson returns the canonical json of the PeerListResponse object */ func (p *PeerListResponse) MarshalJson() ([]byte, error) { return json.MarshalCanonical(p) } /* FetchPeerListResponse returns a new PeerListResponse object from the local peer list */ func (s *State) FetchPeerListResponse() PeerListResponse { list := PeerListResponse{ Type: "peers", Peers: []string{}, } for _, p := range s.PeerList { list.Peers = append(list.Peers, p.Name) } return list } /* FindPeerByName returns the specified peer from the peer list */ func (s *State) FindPeerByName(peerName string) *Peer { for p, peer := range s.PeerList { if peer.Name == peerName { return &s.PeerList[p] } } return nil } /* RemovePeerByName removes the specified peer from the peer list */ func (s *State) RemovePeerByName(peerName string) { for i, peer := range s.PeerList { if peer.Name == peerName { slices.Delete(s.PeerList, i, i) } } s.DumpPeerListStore() } /* ParsePeerListResponse parses the given PeerListResponse object to the local peer list */ func (s *State) ParsePeerListResponse(p *PeerListResponse) { for _, peer := range p.Peers { if s.Validator.IsValidPeerName(peer) && s.FindPeerByName(peer) == nil { p := Peer{ Name: peer, Active: false, } s.PeerList = append(s.PeerList, p) } } s.DumpPeerListStore() } /* DumpPeerListStore stores the local peer list to a JSON file */ func (s *State) DumpPeerListStore() { list := []string{} for _, p := range s.PeerList { if s.Validator.IsValidPeerName(p.Name) { list = append(list, p.Name) } } file, _ := json.MarshalCanonical(list) _ = os.WriteFile(s.Config.PeerListStore, file, 0644) } func (s *State) parsePeerListStore() error { if s.Validator.CheckIfFileExists(s.Config.PeerListStore) { file, err := os.ReadFile(s.Config.PeerListStore) if err != nil { return errors.New("cannot read peer list store " + s.Config.PeerListStore) } peerList := []string{} err = json.Unmarshal([]byte(file), &peerList) if err != nil { return errors.New("cannot parse peer list store at " + s.Config.PeerListStore) } for _, peer := range peerList { if s.Validator.IsValidPeerName(peer) { p := Peer{ Name: peer, Active: false, } s.PeerList = append(s.PeerList, p) } } return nil } return errors.New("cannot find peer list store at " + s.Config.PeerListStore) } func (s *State) parseInitialPeerList() { s.PeerList = []Peer{} if strings.Contains(s.Config.InitialPeerList, ",") { peers := strings.Split(s.Config.InitialPeerList, ",") for _, peerName := range peers { if s.Validator.IsValidPeerName(peerName) { p := Peer{ Name: peerName, Active: false, } s.PeerList = append(s.PeerList, p) } else { s.Logger.Warn("Failed parsing the initial peer list. Please check your configuration") } } } else { if s.Validator.IsValidPeerName(s.Config.InitialPeerList) { p := Peer{ Name: s.Config.InitialPeerList, Active: false, } s.PeerList = append(s.PeerList, p) } else { s.Logger.Warn("Failed parsing the initial peer list. Please check your configuration") } } } /* ====================== PKI OPERATIONS ====================== */ func (s *State) parsePKIFile() (ed25519.PrivateKey, error) { privKeyBytes, err := os.ReadFile(s.Config.PrivateKeyFile) if err != nil { return nil, errors.New("cannot read private key file at " + s.Config.PrivateKeyFile) } res, err := x509.ParsePKCS8PrivateKey(privKeyBytes) if err != nil { return nil, errors.New("cannot parse private key from " + s.Config.PrivateKeyFile) } privKey := res.(ed25519.PrivateKey) return privKey, nil } func (s *State) dumpPKIFile() { privKeyBytes, _ := x509.MarshalPKCS8PrivateKey(s.PrivateKey) _ = os.WriteFile(s.Config.PrivateKeyFile, privKeyBytes, 0600) } /* ====================== BLOCK OPERATIONS ====================== */ func (s *State) validateBlock(b *Block) error { err := b.Validate() if err != nil { return err } if b.IsGenesisBlock() { return nil } bid := b.GetID() prevBlock := s.Chain[len(s.Chain)-1] prevHash, err := prevBlock.Hash() if err != nil { return err } if *b.Previd != prevHash || prevBlock.Created > b.Created { return errors.New("invalid chain extension, block invalid: " + bid) } cbid := "invalid" for i, txid := range b.Txids { tx := s.Transactions[txid] if tx != nil { err = tx.Validate() if err != nil { return errors.New("detected invalid transaction, block invalid: " + bid) } if tx.IsCoinbaseTx() { if i != 0 { return errors.New("invalid coinbase transaction, block invalid: " + bid) } if tx.Outputs[0].Value > s.getTransactionFeesSum(b)+GetBlockReward() { return errors.New("law of conservation for coinbase transaction broken, block invalid: " + bid) } txBlock := s.findBlockForTransaction(txid) if txBlock != nil && *tx.Height != txBlock.Height { return errors.New("incorrect height on coinbase transaction, block invalid: " + bid) } cbid, _ = tx.Hash() } else { if cbid != "invalid" && tx.IsTxInput(cbid) { return errors.New("coinbase transaction cannot be spent in same block, block invalid: " + bid) } } } } return nil } /* AppendToChain adds the specified block to the chain */ func (s *State) AppendToChain(b *Block) error { if len(s.Chain) == 0 { s.Chain = append(s.Chain, *GetGenesisBlock()) s.Height = 0 } bid, err := b.Hash() if err != nil { s.Logger.Debug(err.Error()) return err } if s.GetBlock(bid) != nil { s.Logger.Debug("Block already in chain: " + bid) return nil } s.Logger.Debug("Validating block: " + bid) err = s.validateBlock(b) if err != nil { return err } if b.Previd != nil { prevBlock := s.GetBlock(*b.Previd) b.Height = prevBlock.Height + 1 } else { if b.IsGenesisBlock() { b.Height = GetGenesisBlock().Height } else { return errors.New("invalid genesis block") } } if b.Height > s.Height { s.Logger.Info("New longest chain extension found: " + bid) s.Height = b.Height err = s.computeUTXOSet(b) if err == nil { s.Chain = append(s.Chain, *b) for _, txid := range b.Txids { s.confirmTransaction(txid) } s.DumpTransactionStore() } else { s.Logger.Debug(err.Error()) return err } } return nil } func (s *State) computeUTXOSet(b *Block) error { if b.IsGenesisBlock() { b.UTXOSet = GetGenesisBlock().UTXOSet return nil } s.Logger.Debug("Computing UTXO set for block") prevBlock := s.GetBlock(*b.Previd) b.UTXOSet = make(map[string]uint64) maps.Copy(prevBlock.UTXOSet, b.UTXOSet) for _, txid := range b.Txids { s.Logger.Debug("Computing UTXO for transaction " + txid) transaction := s.GetTransaction(txid) err := transaction.Validate() if err != nil { return errors.New("invalid utxo set for block " + b.GetID() + ", transaction " + txid) } if transaction.IsCoinbaseTx() { output := transaction.Outputs[0] b.UTXOSet[output.Pubkey] = output.Value } else { for _, input := range transaction.Inputs { output := s.getOutputFromTransactionInput(&input) var balance uint64 balance = output.Value if val, ok := b.UTXOSet[output.Pubkey]; ok { balance = val + output.Value } for _, o := range transaction.Outputs { balance -= o.Value } if balance > 0 { b.UTXOSet[output.Pubkey] = balance } else { delete(b.UTXOSet, output.Pubkey) } } } } return nil } /* GetBlock returns the block specified by the supplied ID or nil */ func (s *State) GetBlock(bid string) *Block { for _, b := range s.Chain { if bid == b.GetID() { return &b } } return nil } /* GetChainTip returns the tip of our local chain as an object */ func (s *State) GetChainTip() *Block { if len(s.Chain) == 0 { return GetGenesisBlock() } return &s.Chain[len(s.Chain)-1] } func (s *State) getTransactionFeesSum(b *Block) uint64 { var fees uint64 fees = 0 for _, txid := range b.Txids { fees += s.getTransactionFees(txid) } return fees } /* GetMissingTransactionsInBlock returns the transaction ids from the block that are not present in the mempool */ func (s *State) GetMissingTransactionsInBlock(b *Block) ([]string, error) { err := b.Validate() if err != nil { return []string{}, err } res := []string{} for _, txid := range b.Txids { tx := s.Transactions[txid] if tx == nil { res = append(res, txid) } } return res, nil } /* DumpBlockStore stores the local block chain to a JSON file */ func (s *State) DumpBlockStore() { file, _ := json.MarshalCanonical(s.Chain) _ = os.WriteFile(s.Config.BlockStore, file, 0644) } func (s *State) parseBlockStore() error { if s.Validator.CheckIfFileExists(s.Config.BlockStore) { file, err := os.ReadFile(s.Config.BlockStore) if err != nil { return errors.New("cannot read block store " + s.Config.BlockStore) } chain := []Block{} err = json.Unmarshal([]byte(file), &chain) if err != nil { return errors.New("cannot parse block store at " + s.Config.BlockStore) } for i, block := range chain { if block.IsGenesisBlock() { continue } block.Height = uint64(i) s.AppendToChain(&block) } return nil } return errors.New("cannot find block store at " + s.Config.BlockStore) } /* ============================= TRANSACTION OPERATIONS ============================= */ func (s *State) validateTransaction(t *Transaction) error { txid, err := t.Hash() if err != nil { s.Logger.Debug(err.Error()) return err } if len(t.Outputs) == 0 { return errors.New("outputs cannot be empty, transaction invalid: " + txid) } if t.Height != nil { s.Logger.Debug("Processing a coinbase transaction: " + txid) } else { inputValues := 0 outputValues := 0 if len(t.Inputs) == 0 { return errors.New("inputs cannot be empty in a non-coinbase transaction, transaction invalid: " + txid) } for _, input := range t.Inputs { prevTransaction := s.Transactions[input.Outpoint.Txid] if prevTransaction == nil { return errors.New("could not find output transaction in pool, transaction invalid: " + txid) } if len(prevTransaction.Outputs)-1 < int(input.Outpoint.Index) { return errors.New("could not find output specified by index in pool, transaction invalid: " + txid) } outpoint := prevTransaction.Outputs[int(input.Outpoint.Index)] if !t.VerifySign(input.Sig, outpoint.Pubkey) { return errors.New("could not validate input signatures, transaction invalid: " + txid) } inputValues += int(outpoint.Value) } for _, output := range t.Outputs { outputValues += int(output.Value) } if inputValues != 0 && outputValues > inputValues { return errors.New("overspending, transaction invalid: " + txid) } } return nil } /* AppendTransaction adds the specified transaction to the store */ func (s *State) AppendTransaction(t *Transaction) error { txid, err := t.Hash() if err != nil { s.Logger.Debug(err.Error()) return err } s.Logger.Debug("Validating transaction: " + txid) err = s.validateTransaction(t) if err != nil { s.Logger.Debug(err.Error()) return err } s.Logger.Debug("Appending transaction to store: " + txid) if t.IsCoinbaseTx() { t.Confirmed = true } else { t.Confirmed = false } s.Transactions[txid] = t return nil } func (s *State) findBlockForTransaction(txid string) *Block { for _, block := range s.Chain { if block.ContainsTransaction(txid) { return &block } } return nil } /* GetTransaction returns the specified transaction from the store */ func (s *State) GetTransaction(txid string) *Transaction { if v, ok := s.Transactions[txid]; ok { return v } return nil } /* GetMempoolTransactionIDs returns the list of transaction IDs in the mempool */ func (s *State) GetMempoolTransactionIDs() []string { res := []string{} for txid, tx := range s.Transactions { if !tx.Confirmed { res = append(res, txid) } } return res } func (s *State) getConfirmedTransactions() map[string]*Transaction { res := map[string]*Transaction{} for txid, tx := range s.Transactions { if tx.Confirmed { res[txid] = tx } } return res } func (s *State) confirmTransaction(txid string) { tx := s.GetTransaction(txid) if tx != nil { tx.Confirmed = true } } func (s *State) getTransactionFees(txid string) uint64 { var fees uint64 fees = 0 transaction := s.GetTransaction(txid) if transaction.IsCoinbaseTx() { return 0 } for _, input := range transaction.Inputs { prevOutput := s.getOutputFromTransactionInput(&input) fees += prevOutput.Value } for _, output := range transaction.Outputs { fees -= output.Value } return fees } func (s *State) getOutputFromTransactionInput(i *Input) *Output { txid := i.Outpoint.Txid index := i.Outpoint.Index outputTx := s.GetTransaction(txid) for i, tx := range outputTx.Outputs { if int64(i) == index { return &tx } } return nil } /* DumpTransactionStore stores the local store to a JSON file */ func (s *State) DumpTransactionStore() { file, _ := json.MarshalCanonical(s.getConfirmedTransactions()) _ = os.WriteFile(s.Config.TransactionStore, file, 0644) } func (s *State) parseTransactionStore() error { if s.Validator.CheckIfFileExists(s.Config.TransactionStore) { file, err := os.ReadFile(s.Config.TransactionStore) if err != nil { return errors.New("cannot read transaction store " + s.Config.TransactionStore) } var pool map[string]*Transaction err = json.Unmarshal([]byte(file), &pool) if err != nil { return errors.New("cannot parse transaction store at " + s.Config.TransactionStore) } for id, transaction := range pool { s.Logger.Debug("Appending local transaction to store: " + id + ", tx: " + transaction.String()) transaction.Confirmed = true s.Transactions[id] = transaction } return nil } return errors.New("cannot find transaction store at " + s.Config.TransactionStore) }