Archived
2
0
This repository has been archived on 2023-03-02. You can view files and clone it, but cannot push or open issues or pull requests.
kerma/models/state.go
2023-03-02 15:28:43 +01:00

771 lines
16 KiB
Go

// 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)
}