Archived
2
0

Initial commit

This commit is contained in:
2023-03-02 15:28:43 +01:00
commit 2d4d7759e0
40 changed files with 4249 additions and 0 deletions

231
models/block.go Normal file
View File

@@ -0,0 +1,231 @@
// Package models provides the models for the KermaGo application
package models
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"math"
"time"
"github.com/docker/go/canonical/json"
"golang.org/x/exp/slices"
"golang.org/x/exp/utf8string"
)
/*
A Block is a struct that represents a block as per the Kerma specification
*/
type Block struct {
Type string `json:"type" binding:"required"`
Txids []string `json:"txids" binding:"required"`
Nonce string `json:"nonce" binding:"required"`
Previd *string `json:"previd"`
Created int64 `json:"created" binding:"required"`
Target string `json:"T" binding:"required"`
Miner string `json:"miner,omitempty"`
Note string `json:"note,omitempty"`
Height uint64 `json:"-"`
UTXOSet map[string]uint64 `json:"-"`
}
/*
Hash returns the sha256 hash of the canonical JSON of the block
*/
func (b *Block) Hash() (string, error) {
block, err := b.MarshalJson()
if err != nil {
return "", errors.New("could not parse block as json")
}
hashSum := sha256.Sum256(block)
return hex.EncodeToString(hashSum[:]), nil
}
/*
GetID returns the block id
*/
func (b *Block) GetID() string {
id, err := b.Hash()
if err != nil {
return ""
}
return id
}
/*
GetType returns the type of the block
*/
func (b *Block) GetType() string {
return b.Type
}
/*
GetEntity returns the block
*/
func (b *Block) GetEntity() interface{} {
return b
}
/*
Validate is responsible for validating the block; returns an error if unsuccessful
*/
func (b *Block) Validate() error {
if b.Type != "block" {
return errors.New("object not a block")
}
if b.IsGenesisBlock() {
return nil
}
if b.Previd == nil && !b.IsGenesisBlock() {
return errors.New("illegal genesis block detected")
}
if b.Target != GetGenesisBlock().Target {
return errors.New("incorrect target supplied")
}
if b.Created < GetGenesisBlock().Created {
return errors.New("timestamp before genesis block")
}
// TODO: check if this should be millis
if b.Created > time.Now().UnixMilli() {
return errors.New("block created in the future")
}
if b.Miner != "" {
if !utf8string.NewString(b.Miner).IsASCII() {
return errors.New("miner is not an ascii printable string")
}
if len(b.Miner) > 128 {
return errors.New("miner length incorrect")
}
}
if b.Note != "" {
if !utf8string.NewString(b.Note).IsASCII() {
return errors.New("note is not an ascii printable string")
}
if len(b.Note) > 128 {
return errors.New("note length incorrect")
}
}
_, err := hex.DecodeString(b.Nonce)
if err != nil {
return err
}
_, err = hex.DecodeString(*b.Previd)
if err != nil {
return err
}
bid, err := b.Hash()
if err != nil {
return err
}
bidBytes, _ := hex.DecodeString(bid)
bint := binary.BigEndian.Uint64(bidBytes)
if err != nil {
return err
}
tidBytes, _ := hex.DecodeString(b.Target)
tid := binary.BigEndian.Uint64(tidBytes)
if err != nil {
return err
}
if bint >= tid {
return errors.New("proof-of-work equation not satisfied")
}
return nil
}
/*
String returns the canonical JSON of the block as a string
*/
func (b *Block) String() string {
res, _ := b.MarshalJson()
return string(res)
}
/*
ContainsTransaction checks if the block contains the specified transaction
*/
func (b *Block) ContainsTransaction(txid string) bool {
return slices.Contains(b.Txids, txid)
}
/*
MarshalJson returns the canonical JSON of the block
*/
func (b *Block) MarshalJson() ([]byte, error) {
return json.MarshalCanonical(b)
}
/*
UnmarshalJSON creates a block from the input JSON byte array
*/
func (b *Block) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
type tmp Block
err := json.Unmarshal(data, (*tmp)(b))
if err != nil {
return err
}
err = b.Validate()
if err != nil {
return err
}
return nil
}
/*
IsGenesisBlock checks if the block is the genesis block
*/
func (b *Block) IsGenesisBlock() bool {
return b.String() == GetGenesisBlock().String()
}
/*
GetGenesisBlock returns the genesis block
*/
func GetGenesisBlock() *Block {
genesis := Block{
Type: "block",
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Created: 1624219079,
Miner: "dionyziz",
Nonce: "0000000000000000000000000000000000000000000000000000002634878840",
Note: "The Economist 2021-06-20: Crypto-miners are probably to blame for the graphics-chip shortage",
Previd: nil,
Txids: []string{},
Height: 0,
UTXOSet: map[string]uint64{},
}
return &genesis
}
/*
GetBlockReward returns the block reward
*/
func GetBlockReward() uint64 {
// 5 * (10^13)
return 5 * uint64(math.Pow(10, 13))
}

46
models/chaintip.go Normal file
View File

@@ -0,0 +1,46 @@
// Package models provides the models for the KermaGo application
package models
import (
"github.com/docker/go/canonical/json"
)
/*
A Chaintip is an object that has a type "chaintip" and a block ID
*/
type Chaintip struct {
Type string `json:"type" binding:"required"`
BlockID string `json:"blockid" binding:"required"`
}
/*
Construct creates a new Chaintip object for this instance
*/
func (c *Chaintip) Construct(bid string) {
c.Type = "chaintip"
c.BlockID = bid
}
/*
MarshalJson returns the canonical json of the chaintip object
*/
func (c *Chaintip) MarshalJson() ([]byte, error) {
return json.MarshalCanonical(c)
}
/*
UnmarshalJSON returns a new Chaintip object from a byte array
*/
func (c *Chaintip) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
type tmp Chaintip
err := json.Unmarshal(data, (*tmp)(c))
if err != nil {
return err
}
return nil
}

27
models/error.go Normal file
View File

@@ -0,0 +1,27 @@
// Package models provides the models for the KermaGo application
package models
import "github.com/docker/go/canonical/json"
/*
An Error represents a struct that has a type and an error message as a string
*/
type Error struct {
Type string `json:"type" binding:"required"`
Error string `json:"error" binding:"required"`
}
/*
Construct creates a new error object with the given string as message
*/
func (e *Error) Construct(errorMsg string) {
e.Type = "error"
e.Error = errorMsg
}
/*
MarshalJson returns the canonical json of the error object
*/
func (e *Error) MarshalJson() ([]byte, error) {
return json.MarshalCanonical(e)
}

39
models/generic.go Normal file
View File

@@ -0,0 +1,39 @@
// Package models provides the models for the KermaGo application
package models
import "github.com/docker/go/canonical/json"
/*
A Generic represents a struct that only has a type
*/
type Generic struct {
Type string `json:"type" binding:"required"`
}
/*
MarshalJson returns the canonical json of the generic object
*/
func (g *Generic) MarshalJson() ([]byte, error) {
return json.MarshalCanonical(g)
}
/*
BuildPeerRequest creates a new generic object with type "getpeers"
*/
func (g *Generic) BuildPeerRequest() {
g.Type = "getpeers"
}
/*
BuildChainTipRequest creates a new generic object with type "getchaintip"
*/
func (g *Generic) BuildChainTipRequest() {
g.Type = "getchaintip"
}
/*
BuildMempoolRequest creates a new generic object with type "getmempool"
*/
func (g *Generic) BuildMempoolRequest() {
g.Type = "getmempool"
}

69
models/hello.go Normal file
View File

@@ -0,0 +1,69 @@
// Package models provides the models for the KermaGo application
package models
import (
"errors"
"regexp"
"github.com/docker/go/canonical/json"
)
/*
A Hello represents a struct that has a type, a version and a user agent
*/
type Hello struct {
Type string `json:"type" binding:"required"`
Version string `json:"version" binding:"required"`
Agent string `json:"agent"`
}
/*
Construct creates a new Hello object for this instance
*/
func (h *Hello) Construct() {
h.Type = "hello"
h.Version = "0.8.0"
h.Agent = "BadKerma Go Client 0.8.x"
}
/*
MarshalJson returns the canonical json of the hello object
*/
func (h *Hello) MarshalJson() ([]byte, error) {
return json.MarshalCanonical(h)
}
/*
UnmarshalJSON returns a new Hello request from a byte array
*/
func (h *Hello) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
type tmp Hello
err := json.Unmarshal(data, (*tmp)(h))
if err != nil {
return err
}
if !h.verify() {
return errors.New("hello request not valid")
}
return nil
}
func (h *Hello) verify() bool {
if h.Type == "hello" && h.isValidVersion() {
return true
}
return false
}
func (h *Hello) isValidVersion() bool {
RegExp := regexp.MustCompile(`^0\.8\.[0-9]$`)
return RegExp.MatchString(h.Version)
}

29
models/interfaces.go Normal file
View File

@@ -0,0 +1,29 @@
// Package models provides the models for the KermaGo application
package models
/*
An IEntity is an interface that represents a Kerma entity - either a Block or a Transaction
*/
type IEntity interface {
Hash() (string, error)
GetType() string
GetID() string
GetEntity() interface{}
Validate() error
String() string
}
/*
An IObject is an interface that represents a Kerma object
*/
type IObject interface {
GetObjectValue() (IEntity, error)
String() string
}
/*
A CustomMarshaler is an interface that provides custom marshaling functions
*/
type CustomMarshaler interface {
MarshalJson() ([]byte, error)
}

57
models/mempool.go Normal file
View File

@@ -0,0 +1,57 @@
// Package models provides the models for the KermaGo application
package models
import (
"errors"
"github.com/docker/go/canonical/json"
"golang.org/x/exp/slices"
)
/*
A Mempool represents a struct that has a type, and a list of tx ids
*/
type Mempool struct {
Type string `json:"type" binding:"required"`
Txids []string `json:"txids" binding:"required"`
}
/*
Construct creates a new Mempool object for this instance
*/
func (m *Mempool) Construct(mempool []string) {
m.Type = "mempool"
m.Txids = slices.Clone(mempool)
}
/*
MarshalJson returns the canonical json of the mempool object
*/
func (m *Mempool) MarshalJson() ([]byte, error) {
return json.MarshalCanonical(m)
}
/*
UnmarshalJSON returns a new Mempool request from a byte array
*/
func (m *Mempool) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
type tmp Mempool
err := json.Unmarshal(data, (*tmp)(m))
if err != nil {
return err
}
if !m.verify() {
return errors.New("mempool request not valid")
}
return nil
}
func (m *Mempool) verify() bool {
return m.Type == "mempool"
}

128
models/object.go Normal file
View File

@@ -0,0 +1,128 @@
// Package models provides the models for the KermaGo application
package models
import (
"errors"
"github.com/docker/go/canonical/json"
)
/*
An Object is a struct that represents a Kerma object. The object has a type and the object specification as a raw JSON
*/
type Object struct {
Type string `json:"type" binding:"required"`
Object json.RawMessage `json:"object" binding:"required"`
}
/*
An ObjectWrapper is a struct that wraps a Kerma object. The wrapper has a type and an object ID
*/
type ObjectWrapper struct {
Type string `json:"type" binding:"required"`
ObjectID string `json:"objectid" binding:"required"`
}
/*
BuildObjectRequest creates a new ObjectWrapper with type "getobject" and the supplied Object ID
*/
func (o *ObjectWrapper) BuildObjectRequest(oid string) {
o.Type = "getobject"
o.ObjectID = oid
}
/*
BuildGossipObject creates a new ObjectWrapper with type "ihaveobject" and the supplied Object ID
*/
func (o *ObjectWrapper) BuildGossipObject(oid string) {
o.Type = "ihaveobject"
o.ObjectID = oid
}
/*
MarshalJson returns the canonical JSON of the object wrapper
*/
func (o *ObjectWrapper) MarshalJson() ([]byte, error) {
return json.MarshalCanonical(o)
}
/*
UnmarshalJSON creates a new ObjectWrapper from the input JSON byte array
*/
func (o *ObjectWrapper) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
type tmp ObjectWrapper
err := json.Unmarshal(data, (*tmp)(o))
if err != nil {
return err
}
if o.Type != "getobject" && o.Type != "ihaveobject" {
return errors.New("inccorect type for object supplied")
}
return nil
}
/*
GetObjectValue returns the value of the object as an IEntity
*/
func (o *Object) GetObjectValue() (IEntity, error) {
msg, err := o.Object.MarshalJSON()
if err != nil {
return nil, err
}
var transaction Transaction
terr := transaction.UnmarshalJSON(msg)
if terr == nil {
return &transaction, nil
}
var block Block
berr := block.UnmarshalJSON(msg)
if berr == nil {
return &block, nil
}
return nil, berr
}
/*
MarshalJson returns the canonical JSON of the object
*/
func (o *Object) MarshalJson() ([]byte, error) {
return json.MarshalCanonical(o)
}
/*
UnmarshalJSON creates a new object from the input JSON byte array
*/
func (o *Object) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
type tmp Object
err := json.Unmarshal(data, (*tmp)(o))
if err != nil {
return err
}
if o.Type != "object" {
return errors.New("inccorect type for object supplied")
}
return nil
}
/*
String returns the canonical JSON of the object as a string
*/
func (o *Object) String() string {
res, _ := o.MarshalJson()
return string(res)
}

770
models/state.go Normal file
View File

@@ -0,0 +1,770 @@
// 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)
}

292
models/tests/block_test.go Normal file
View File

@@ -0,0 +1,292 @@
package tests
import (
"crypto/sha256"
"encoding/hex"
"kerma/models"
"testing"
"time"
"github.com/docker/go/canonical/json"
. "github.com/smartystreets/goconvey/convey"
)
func TestBlock(t *testing.T) {
Convey("Given a new block", t, func() {
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
txid := "1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e374af"
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: 1649827795114,
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "svatsan",
Note: "First block. Yayy, I have 50 bu now!!",
}
Convey("When GetType is called, 'block' should be returned", func() {
So(block.GetType(), ShouldEqual, "block")
})
Convey("When GetEntity is called, the block should be returned", func() {
So(block.GetEntity(), ShouldResemble, &block)
})
Convey("When Validate is called, no error should be returned", func() {
So(block.Validate(), ShouldEqual, nil)
})
Convey("When ContainsTransaction is called with a correct id, 'true' should be returned", func() {
So(block.ContainsTransaction(txid), ShouldEqual, true)
})
Convey("When ContainsTransaction is called with an incorrect id, 'false' should be returned", func() {
So(block.ContainsTransaction("123"), ShouldEqual, false)
})
Convey("When Hash is called, the hash of the cannonical json and no error should be returned", func() {
blockJSON, _ := json.MarshalCanonical(block)
hashSum := sha256.Sum256(blockJSON)
res, err := block.Hash()
So(hex.EncodeToString(hashSum[:]), ShouldEqual, res)
So(err, ShouldEqual, nil)
})
})
Convey("Given the genesis block", t, func() {
block := models.GetGenesisBlock()
gid := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
Convey("When GetID is called, the block id should be correct", func() {
bid := block.GetID()
So(bid, ShouldEqual, gid)
})
Convey("When Validate is called, no error should be returned", func() {
So(block.Validate(), ShouldBeNil)
})
})
Convey("Given a new incorrect block", t, func() {
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
txid := "1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e374af"
Convey("When the block is a genesis", func() {
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: nil,
Created: 1649827795114,
Target: "reallyillegalgenesisblock",
Miner: "svatsan",
Note: "",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldEqual, "illegal genesis block detected")
})
})
Convey("With an incorrect genesis", func() {
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: 1649827795114,
Target: "reallyillegalgenesisblock",
Miner: "svatsan",
Note: "",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldEqual, "incorrect target supplied")
})
})
Convey("With a timestamp before genesis", func() {
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: 0,
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "svatsan",
Note: "",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldEqual, "timestamp before genesis block")
})
})
Convey("With a timestamp in the future", func() {
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: time.Now().UnixMilli() + 10000000,
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "svatsan",
Note: "",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldEqual, "block created in the future")
})
})
Convey("With a miner that is a non-ASCII printible string", func() {
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: time.Now().UnixMilli(),
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "Not@nħCIIStrIng",
Note: "",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldEqual, "miner is not an ascii printable string")
})
})
Convey("With a miner that is more than 128 characters long", func() {
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: time.Now().UnixMilli(),
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "LongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongMinerName",
Note: "",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldEqual, "miner length incorrect")
})
})
Convey("With a note that is a non-ASCII printible string", func() {
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: time.Now().UnixMilli(),
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "",
Note: "Not@nħCIIStrIng",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldEqual, "note is not an ascii printable string")
})
})
Convey("With a note that is more than 128 characters long", func() {
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: time.Now().UnixMilli(),
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "",
Note: "LongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongNote",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldEqual, "note length incorrect")
})
})
Convey("With a non-hex encoded nonce", func() {
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "AReallyIncorrectNonce",
Previd: &previd,
Created: time.Now().UnixMilli(),
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "",
Note: "",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldNotBeNil)
})
})
Convey("With a non-hex encoded previd", func() {
incorrectPrevId := "incorrectprevid"
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &incorrectPrevId,
Created: time.Now().UnixMilli(),
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "",
Note: "",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldNotBeNil)
})
})
Convey("With an incorrect proof-of-work equation", func() {
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
txid := "1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e37412"
block := models.Block{
Type: "block",
Txids: []string{
txid,
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: 1649827795114,
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "svatsan",
Note: "First block. Yayy, I have 50 bu now!!",
}
Convey("When Validate is called, an error should be returned", func() {
So(block.Validate().Error(), ShouldNotBeNil)
})
})
})
}

View File

@@ -0,0 +1,36 @@
package tests
import (
"kerma/models"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestChaintip(t *testing.T) {
Convey("Given a new chaintip", t, func() {
blockID := models.GetGenesisBlock().GetID()
var chaintip models.Chaintip
Convey("When the constructor is called with a value", func() {
chaintip.Construct(blockID)
Convey("The value should be set", func() {
So(chaintip.Type, ShouldEqual, "chaintip")
So(chaintip.BlockID, ShouldEqual, blockID)
})
})
Convey("When the MarshalJson is called", func() {
chaintip.Construct(blockID)
Convey("The canonical json and no error should be returned", func() {
chaintipJSON, err := chaintip.MarshalJson()
canonJSON := `{"blockid":"` + blockID + `","type":"chaintip"}`
So(string(chaintipJSON), ShouldEqualJSON, canonJSON)
So(err, ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,24 @@
package tests
import (
"kerma/models"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestError(t *testing.T) {
Convey("Given a new error", t, func() {
errString := "testing error"
var err models.Error
Convey("When the constructor is called with a value", func() {
err.Construct(errString)
Convey("The value should be set", func() {
So(err.Type, ShouldEqual, "error")
So(err.Error, ShouldEqual, errString)
})
})
})
}

View File

@@ -0,0 +1,22 @@
package tests
import (
"kerma/models"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestGeneric(t *testing.T) {
Convey("Given a new generic request", t, func() {
var generic models.Generic
Convey("When the BuildPeerRequest method is called", func() {
generic.BuildPeerRequest()
Convey("The type should be 'getpeers'", func() {
So(generic.Type, ShouldEqual, "getpeers")
})
})
})
}

View File

@@ -0,0 +1,36 @@
package tests
import (
"errors"
"kerma/models"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestHello(t *testing.T) {
Convey("Given a new hello request", t, func() {
var hello models.Hello
Convey("When the Construct method is called", func() {
hello.Construct()
Convey("A correct hello request should be created", func() {
So(hello.Type, ShouldEqual, "hello")
So(hello.Version, ShouldEqual, "0.8.0")
So(hello.Agent, ShouldEqual, "BadKerma Go Client 0.8.x")
})
})
})
Convey("When an invalid request is created", t, func() {
var hello models.Hello
msgJSON := `{"type": "hello", "version": "0.9.0"}`
Convey("When UnmarshalJSON is called, it should return an error", func() {
err := errors.New("hello request not valid")
So(hello.UnmarshalJSON([]byte(msgJSON)), ShouldResemble, err)
})
})
}

View File

@@ -0,0 +1,55 @@
package tests
import (
"kerma/models"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestMempool(t *testing.T) {
Convey("Given a new regular transaction", t, func() {
transaction := models.Transaction{
Type: "transaction",
Inputs: []models.Input{
{
Outpoint: models.Outpoint{
Index: 0,
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
},
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
},
},
Outputs: []models.Output{
{
Pubkey: "857debb2084fc8c87dec10d305993e781d9c9dbf6a81762b2f245095ae6b8fb9",
Value: 50,
},
},
}
txid := transaction.GetID()
var mempool models.Mempool
Convey("When the constructor is called with a value", func() {
mempool.Construct([]string{txid})
Convey("The value should be set", func() {
So(mempool.Type, ShouldEqual, "mempool")
So(mempool.Txids[0], ShouldEqual, txid)
})
})
Convey("When the MarshalJson is called", func() {
mempool.Construct([]string{txid})
Convey("The canonical json and no error should be returned", func() {
mempoolJSON, err := mempool.MarshalJson()
canonJSON := `{"txids":["` + txid + `"],"type":"mempool"}`
So(string(mempoolJSON), ShouldEqualJSON, canonJSON)
So(err, ShouldBeNil)
})
})
})
}

147
models/tests/object_test.go Normal file
View File

@@ -0,0 +1,147 @@
package tests
import (
"crypto/sha256"
"encoding/hex"
"kerma/models"
"testing"
"github.com/docker/go/canonical/json"
. "github.com/smartystreets/goconvey/convey"
)
func TestObject(t *testing.T) {
Convey("Given a new object wrapper", t, func() {
var wrapper models.ObjectWrapper
toHash := "thisstringwillbehashed"
hashSum := sha256.Sum256([]byte(toHash))
oid := hex.EncodeToString(hashSum[:])
Convey("When the BuildObjectRequest method is called", func() {
wrapper.BuildObjectRequest(oid)
Convey("The object wrapper should be a valid 'getobject' object", func() {
So(wrapper.Type, ShouldEqual, "getobject")
So(wrapper.ObjectID, ShouldEqual, oid)
})
})
Convey("When the BuildGossipObject method is called", func() {
wrapper.BuildGossipObject(oid)
Convey("The object wrapper should be a valid 'ihaveobject' object", func() {
So(wrapper.Type, ShouldEqual, "ihaveobject")
So(wrapper.ObjectID, ShouldEqual, oid)
})
})
})
Convey("Given a new object", t, func() {
var object models.Object
Convey("When the object is a valid transaction", func() {
raw := json.RawMessage(`
{
"height":1,
"outputs":[
{
"pubkey":"62b7c521cd9211579cf70fd4099315643767b96711febaa5c76dc3daf27c281c",
"value":50000000000000
}
],
"type":"transaction"
}`)
height := uint64(1)
object = models.Object{
Type: "object",
Object: raw,
}
transaction := models.Transaction{
Type: "transaction",
Height: &height,
Outputs: []models.Output{
{
Pubkey: "62b7c521cd9211579cf70fd4099315643767b96711febaa5c76dc3daf27c281c",
Value: 50000000000000,
},
},
}
Convey("Calling GetObjectValue should return the transaction and no error", func() {
res, err := object.GetObjectValue()
So(res, ShouldResemble, &transaction)
So(err, ShouldEqual, nil)
Convey("Calling String should return the string representation of the transaction", func() {
So(res.String(), ShouldEqual, transaction.String())
})
})
})
Convey("When the object is a valid block", func() {
previd := "00000000a420b7cefa2b7730243316921ed59ffe836e111ca3801f82a4f5360e"
raw := json.RawMessage(`
{
"type": "block",
"txids": [
"1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e374af"
],
"nonce": "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
"previd": "` + previd + `",
"created": 1649827795114,
"T": "00000002af000000000000000000000000000000000000000000000000000000",
"miner": "svatsan",
"note": "First block. Yayy, I have 50 bu now!!"
}`)
object = models.Object{
Type: "object",
Object: raw,
}
block := models.Block{
Type: "block",
Txids: []string{
"1bb37b637d07100cd26fc063dfd4c39a7931cc88dae3417871219715a5e374af",
},
Nonce: "c5ee71be4ca85b160d352923a84f86f44b7fc4fe60002214bc1236ceedc5c615",
Previd: &previd,
Created: 1649827795114,
Target: "00000002af000000000000000000000000000000000000000000000000000000",
Miner: "svatsan",
Note: "First block. Yayy, I have 50 bu now!!",
}
Convey("Calling GetObjectValue should return the block and no error", func() {
res, err := object.GetObjectValue()
So(err, ShouldEqual, nil)
So(res, ShouldResemble, &block)
Convey("Calling String should return the string representation of the block", func() {
So(res.String(), ShouldEqual, block.String())
})
})
})
Convey("When the object is a random object", func() {
raw := json.RawMessage(`
{
"key": "value"
}`)
object = models.Object{
Type: "object",
Object: raw,
}
Convey("Calling GetObjectValue should return a nil object and an error", func() {
res, err := object.GetObjectValue()
So(res, ShouldEqual, nil)
So(err, ShouldNotEqual, nil)
})
})
})
}

421
models/tests/state_test.go Normal file
View File

@@ -0,0 +1,421 @@
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)
})
})
})
})
}

View File

@@ -0,0 +1,202 @@
package tests
import (
"crypto/sha256"
"encoding/hex"
"kerma/models"
"testing"
"github.com/docker/go/canonical/json"
. "github.com/smartystreets/goconvey/convey"
)
func TestTransaction(t *testing.T) {
Convey("Given a new coinbase transaction", t, func() {
height := uint64(1)
transaction := models.Transaction{
Type: "transaction",
Height: &height,
Outputs: []models.Output{
{
Pubkey: "62b7c521cd9211579cf70fd4099315643767b96711febaa5c76dc3daf27c281c",
Value: 50000000000000,
},
},
}
Convey("When GetType is called, 'transaction' should be returned", func() {
So(transaction.GetType(), ShouldEqual, "transaction")
})
Convey("When GetEntity is called, the transaction should be returned", func() {
So(transaction.GetEntity(), ShouldResemble, &transaction)
})
Convey("When Validate is called, no error should be returned", func() {
So(transaction.Validate(), ShouldEqual, nil)
})
Convey("When IsCoinbaseTx is called, 'true' should be returned", func() {
So(transaction.IsCoinbaseTx(), ShouldEqual, true)
})
Convey("When IsTxInput is called, 'false' should be returned", func() {
So(transaction.IsTxInput("dasdfasfds"), ShouldEqual, false)
})
Convey("When Hash is called, the hash of the cannonical json and no error should be returned", func() {
transactionJSON, _ := json.MarshalCanonical(transaction)
hashSum := sha256.Sum256(transactionJSON)
res, err := transaction.Hash()
So(hex.EncodeToString(hashSum[:]), ShouldEqual, res)
So(err, ShouldEqual, nil)
})
})
Convey("Given a new regular transaction", t, func() {
transaction := models.Transaction{
Type: "transaction",
Inputs: []models.Input{
{
Outpoint: models.Outpoint{
Index: 0,
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
},
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
},
},
Outputs: []models.Output{
{
Pubkey: "857debb2084fc8c87dec10d305993e781d9c9dbf6a81762b2f245095ae6b8fb9",
Value: 50,
},
},
}
Convey("When GetType is called, 'transaction' should be returned", func() {
So(transaction.GetType(), ShouldEqual, "transaction")
})
Convey("When GetEntity is called, the transaction should be returned", func() {
So(transaction.GetEntity(), ShouldResemble, &transaction)
})
Convey("When Validate is called, no error should be returned", func() {
So(transaction.Validate(), ShouldEqual, nil)
})
Convey("When IsCoinbaseTx is called, 'false' should be returned", func() {
So(transaction.IsCoinbaseTx(), ShouldEqual, false)
})
Convey("When IsTxInput is called with an input, 'true' should be returned", func() {
So(transaction.IsTxInput("2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d"), ShouldEqual, true)
})
Convey("When IsTxInput is called with a non-input, 'false' should be returned", func() {
So(transaction.IsTxInput("dasdfasfds"), ShouldEqual, false)
})
Convey("When Hash is called, the hash of the cannonical json and no error should be returned", func() {
transactionJSON, _ := json.MarshalCanonical(transaction)
hashSum := sha256.Sum256(transactionJSON)
res, err := transaction.Hash()
So(hex.EncodeToString(hashSum[:]), ShouldEqual, res)
So(err, ShouldEqual, nil)
})
Convey("When VerifySign is called with a correct signature, 'true' should be returned", func() {
So(transaction.VerifySign("1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909", "57558a6dae91ac3ab8caf3f543eac9c51cba4ac680ba5ba0d81b5575dc06bc46"), ShouldEqual, true)
})
Convey("When VerifySign is called with an incorrect signature, 'false' should be returned", func() {
So(transaction.VerifySign("", ""), ShouldEqual, false)
})
})
Convey("Given a new incorrect regular transaction", t, func() {
Convey("With two inputs pointing to the same outpoint", func() {
transaction := models.Transaction{
Type: "transaction",
Inputs: []models.Input{
{
Outpoint: models.Outpoint{
Index: 0,
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
},
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
},
{
Outpoint: models.Outpoint{
Index: 0,
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
},
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
},
},
Outputs: []models.Output{
{
Pubkey: "857debb2084fc8c87dec10d305993e781d9c9dbf6a81762b2f245095ae6b8fb9",
Value: 50,
},
},
}
Convey("When Validate is called, an error should be returned", func() {
So(transaction.Validate().Error(), ShouldEqual, "multiple transaction inputs with same outpoint found")
})
})
Convey("With negative outpoint index value", func() {
transaction := models.Transaction{
Type: "transaction",
Inputs: []models.Input{
{
Outpoint: models.Outpoint{
Index: -10,
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
},
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
},
},
Outputs: []models.Output{
{
Pubkey: "857debb2084fc8c87dec10d305993e781d9c9dbf6a81762b2f245095ae6b8fb9",
Value: 50,
},
},
}
Convey("When Validate is called, an error should be returned", func() {
So(transaction.Validate().Error(), ShouldEqual, "transaction input index cannot be negative")
})
})
Convey("With a pubkey that is not hex string", func() {
transaction := models.Transaction{
Type: "transaction",
Inputs: []models.Input{
{
Outpoint: models.Outpoint{
Index: 0,
Txid: "2fb7adb654b373e85c6b5c596cc110dcb6643ee138768f4aa947e9ddb7d91f8d",
},
Sig: "1bc4c05ec180932f08b95a8b5be308bb7b90c4d047720c4953440ea7cf56ba38b7e3b52ae586b594a6ae6649d8be0ae3d6944ffe9a7c5894622c33b9df276909",
},
},
Outputs: []models.Output{
{
Pubkey: "VeryInvalidPubkeyValueWOWThisIsSoInvalidOhMyGod",
Value: 50,
},
},
}
Convey("When Validate is called, an error should be returned", func() {
So(transaction.Validate(), ShouldNotBeNil)
})
})
})
}

251
models/transaction.go Normal file
View File

@@ -0,0 +1,251 @@
// Package models provides the models for the KermaGo application
package models
import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/hex"
"errors"
"kerma/helpers"
"strconv"
"github.com/docker/go/canonical/json"
)
/*
An Outpoint is a struct that represents an outpoint as per the Kerma specification
*/
type Outpoint struct {
Txid string `json:"txid" binding:"required"`
Index int64 `json:"index" binding:"required"`
}
/*
An Input is a struct that represents an input as per the Kerma specification
*/
type Input struct {
Outpoint Outpoint `json:"outpoint" binding:"required"`
Sig string `json:"sig" binding:"required"`
}
type inputVerifier struct {
Outpoint Outpoint `json:"outpoint" binding:"required"`
Sig *string `json:"sig" binding:"required"`
}
/*
An Output is a struct that represents an output as per the Kerma specification
*/
type Output struct {
Pubkey string `json:"pubkey" binding:"required"`
Value uint64 `json:"value" binding:"required"`
}
/*
A Transaction is a struct that represents a transaction as per the Kerma specification
*/
type Transaction struct {
Type string `json:"type" binding:"required"`
Height *uint64 `json:"height,omitempty"`
Inputs []Input `json:"inputs,omitempty"`
Outputs []Output `json:"outputs,omitempty"`
Confirmed bool `json:"-"`
}
type transactionVerifier struct {
Type string `json:"type" binding:"required"`
Inputs []inputVerifier `json:"inputs,omitempty"`
Outputs []Output `json:"outputs,omitempty"`
}
/*
Hash returns the sha256 hash of the canonical JSON of the transaction
*/
func (t *Transaction) Hash() (string, error) {
transaction, err := t.MarshalJson()
if err != nil {
return "", errors.New("could not parse transaction as json")
}
hashSum := sha256.Sum256(transaction)
return hex.EncodeToString(hashSum[:]), nil
}
/*
GetID returns the transaction id
*/
func (b *Transaction) GetID() string {
id, err := b.Hash()
if err != nil {
return ""
}
return id
}
/*
GetType returns the type of the transaction
*/
func (t *Transaction) GetType() string {
return t.Type
}
/*
GetEntity returns the transaction
*/
func (t *Transaction) GetEntity() interface{} {
return t
}
/*
MarshalJson returns the canonical JSON of the transaction
*/
func (t *Transaction) MarshalJson() ([]byte, error) {
return json.MarshalCanonical(t)
}
/*
UnmarshalJSON creates a transaction from the input JSON byte array
*/
func (t *Transaction) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
type tmp Transaction
err := json.Unmarshal(data, (*tmp)(t))
if err != nil {
return err
}
err = t.Validate()
if err != nil {
return err
}
return nil
}
/*
Validate is responsible for validating the transaction; returns an error if unsuccessful
*/
func (t *Transaction) Validate() error {
if t.Type != "transaction" {
return errors.New("object not a transaction")
}
if t.IsCoinbaseTx() {
if len(t.Inputs) > 0 {
return errors.New("coinbase transactions cannot have an input")
}
if len(t.Outputs) != 1 {
return errors.New("coinbase transactions must have exactly one output")
}
}
txOutpoints := map[string]int{}
for _, input := range t.Inputs {
if input.Outpoint.Index < 0 {
return errors.New("transaction input index cannot be negative")
}
outpointId := input.Outpoint.Txid + "-" + strconv.FormatInt(input.Outpoint.Index, 10)
if _, ok := txOutpoints[outpointId]; ok {
return errors.New("multiple transaction inputs with same outpoint found")
}
txOutpoints[outpointId] = 1
}
for _, output := range t.Outputs {
if output.Value < 0 {
return errors.New("transaction output value cannot be negative")
}
pubkeyBytes, err := hex.DecodeString(output.Pubkey)
if err != nil {
return err
}
if ed25519.PublicKey(string(pubkeyBytes)) == nil {
return errors.New("invalid public key value, transaction invalid")
}
}
return nil
}
/*
String returns the canonical JSON of the transaction as a string
*/
func (t *Transaction) String() string {
res, _ := t.MarshalJson()
return string(res)
}
/*
VerifySign verifies whether the signature of the transaction with the provided pubkey matches the signature provided
*/
func (t *Transaction) VerifySign(sig string, pubkeyString string) bool {
var logger helpers.Logger
newTransaction := transactionVerifier{
Type: t.Type,
Inputs: []inputVerifier{},
Outputs: t.Outputs,
}
for _, i := range t.Inputs {
newInput := inputVerifier{
Outpoint: i.Outpoint,
Sig: nil,
}
newTransaction.Inputs = append(newTransaction.Inputs, newInput)
}
txBytes := new(bytes.Buffer)
encoder := json.NewEncoder(txBytes)
encoder.Canonical()
encoder.Encode(newTransaction)
pubkeyBytes, err := hex.DecodeString(pubkeyString)
if err != nil {
return false
}
sigBytes, err := hex.DecodeString(sig)
if err != nil {
return false
}
logger.Debug("Checking transaction signature with params: SIGNATURE(" + sig + "), PUBKEY(" + pubkeyString + "), TRANSACTION(" + txBytes.String() + ")")
pubkey := ed25519.PublicKey(string(pubkeyBytes))
if len(pubkey) != ed25519.PublicKeySize {
return false
}
return ed25519.Verify(ed25519.PublicKey(pubkey), txBytes.Bytes(), sigBytes)
}
/*
IsTxInput checks if the transaction ID supplied is an input of the current transaction
*/
func (t *Transaction) IsTxInput(txid string) bool {
for _, input := range t.Inputs {
if txid == input.Outpoint.Txid {
return true
}
}
return false
}
/*
IsCoinbaseTx checks if the transaction is a coinbase transaction
*/
func (t *Transaction) IsCoinbaseTx() bool {
return t.Height != nil
}