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