Minor changes

This commit is contained in:
Dmitry Anderson 2024-11-24 15:04:52 +01:00
parent 4e044cdad9
commit 3c069df5c1
24 changed files with 580 additions and 177 deletions

8
.idea/.gitignore vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

50
common/base_errors.go Normal file
View File

@ -0,0 +1,50 @@
package common
import (
"errors"
"github.com/matchsystems/werr"
"net/http"
)
var (
ErrNotImplemented = errors.New("not implemented yet")
ErrCanceled = errors.New("canceled")
ErrDeadlineExceeded = errors.New("deadline exceeded")
ErrRemoteServiceFailed = errors.New("remote service failed")
ErrInvalidAction = errors.New("invalid action")
ErrInvalidArgument = errors.New("invalid argument")
ErrOutOfRange = errors.New("out of range")
ErrPermissionDenied = errors.New("permission denied")
ErrUnauthenticated = errors.New("unauthenticated")
ErrEntityExists = errors.New("entity already exists")
ErrEntityNotFound = errors.New("entity not found")
ErrEntityOutdated = errors.New("entity outdated")
)
func GetHttpCode(err error) int {
err = werr.UnwrapAll(err)
switch {
case errors.Is(err, ErrNotImplemented):
return http.StatusNotImplemented
case errors.Is(err, ErrCanceled), errors.Is(err, ErrDeadlineExceeded):
return http.StatusRequestTimeout
case errors.Is(err, ErrRemoteServiceFailed):
return http.StatusServiceUnavailable
case errors.Is(err, ErrInvalidAction), errors.Is(err, ErrInvalidArgument), errors.Is(err, ErrOutOfRange):
return http.StatusBadRequest
case errors.Is(err, ErrPermissionDenied):
return http.StatusForbidden
case errors.Is(err, ErrUnauthenticated):
return http.StatusUnauthorized
case errors.Is(err, ErrEntityExists):
return http.StatusConflict
case errors.Is(err, ErrEntityNotFound):
return http.StatusNotFound
default:
return http.StatusInternalServerError
}
}

View File

@ -1,10 +1,138 @@
package common
import (
"bytes"
"crypto/sha512"
"encoding/binary"
"encoding/json"
"github.com/matchsystems/werr"
"io"
"time"
)
// BlockSecsDiff is the time in second that passes
// between blocks rotation
const BlockSecsDiff = 100
func GetBlockId(t time.Time) int64 {
// because number is int -- it will just cut off the floating point part
return (t.UTC().Unix() / BlockSecsDiff) * BlockSecsDiff
}
type BlockJson struct {
Id int64 `json:"id"`
PrevHash string `json:"prev_hash"`
Hash string `json:"hash"`
TXs []json.RawMessage `json:"txs"`
IsFinished bool `json:"is_finished"`
ByteEncoding string `json:"byte_encoding"`
}
type Block struct {
Id int64
PrevHash []byte
Hash []byte
Id int64
PrevHash []byte
Hash []byte
TXs []*Tx
IsFinished bool
}
func (block *Block) CalcHash() ([]byte, error) {
hash := sha512.New()
if err := binary.Write(hash, binary.BigEndian, block.Id); err != nil {
return nil, werr.Wrapf(err, "failed to write block id into hash buffer for block %d", block.Id)
}
hash.Write(block.PrevHash)
for _, tx := range block.TXs {
hash.Write(tx.Hash)
}
return hash.Sum(nil), nil
}
// ValidateHash calculates new block hash and compares it to current block.Hash
// returning is block hash is valid
func (block *Block) ValidateHash() bool {
hash, err := block.CalcHash()
if err != nil {
return false
}
return bytes.Equal(block.Hash, hash)
}
// ValidateTXs returns the number of invalid transactions
// and map of hashes : reasons why txs were invalid.
// If invalid count > 0 -- the whole block isn't legit
func (block *Block) ValidateTXs() (invalidCount int, invalidTXs map[string]string) {
invalidTXs = make(map[string]string)
for _, tx := range block.TXs {
if err := tx.Validate(); err != nil {
invalidTXs[tx.HashString()] = err.Error()
invalidCount++
}
}
return invalidCount, invalidTXs
}
func (block *Block) ToJSON(byteEncodingStr string) ([]byte, error) {
encoding, err := GetEncoding(byteEncodingStr)
if err != nil {
return nil, err
}
var txData json.RawMessage
txsData := make([]json.RawMessage, len(block.TXs))
for i, tx := range block.TXs {
if txData, err = tx.ToJSON(byteEncodingStr); err != nil {
return nil, werr.Wrapf(err, "error while converting transaction to JSON %d", i)
}
txsData[i] = txData
}
data := &BlockJson{
Id: block.Id,
PrevHash: encoding.EncodeToString(block.PrevHash),
Hash: encoding.EncodeToString(block.Hash),
TXs: txsData,
IsFinished: block.IsFinished,
}
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, werr.Wrapf(err, "error while marshaling")
}
return jsonBytes, err
}
func (block *BlockJson) Process() (*Block, error) {
var err error
txs := make([]*Tx, len(block.TXs))
for i, jsonTx := range block.TXs {
if txs[i], err = TxFromJSON(jsonTx); err != nil {
return nil, err
}
}
out := &Block{
Id: block.Id,
TXs: txs,
IsFinished: block.IsFinished,
}
encoding, err := GetEncoding(block.ByteEncoding)
if err != nil {
return nil, err
}
if out.Hash, err = encoding.DecodeString(block.Hash); err != nil {
return nil, werr.Wrapf(err, "error while decoding hash")
}
return out, nil
}
func BlockFromJSON(marshaledData []byte) (opts *Block, err error) {
data := &BlockJson{}
if err := json.Unmarshal(marshaledData, data); err != nil {
return nil, werr.Wrapf(err, "error while unmarshaling")
}
return data.Process()
}
func BlockFromJsonReader(r io.Reader) (opts *Block, err error) {
data := &BlockJson{}
if err := json.NewDecoder(r).Decode(&data); err != nil {
return nil, werr.Wrapf(err, "error while unmarshaling")
}
return data.Process()
}

3
common/block_test.go Normal file
View File

@ -0,0 +1,3 @@
package common
// TODO

View File

@ -7,3 +7,18 @@ const BurningRate = 0.001
func CalcBurning(trxAmount float64) float64 {
return BurningRate * math.Sqrt(trxAmount)
}
// ValidateBurning doesn't return valid/invalid like usual Validate functions.
// Instead, it returns difference between expected and set values.
// Bcs of a differences in how programming languages might be implemented
// or approach numeric types (yes, JS, I'm about u) we might not get exactly the same burning amount,
// but still it should be a very close value.
// To get boolean value -- use ValidateBurningBool instead
func ValidateBurning(tx *Tx) float64 {
return CalcBurning(tx.Amount) - tx.AmountBurned
}
func ValidateBurningBool(tx *Tx) bool {
// Let's say, that before that count of digits we shouldn't have any mistake
return math.Abs(ValidateBurning(tx)) < 0.1e-10
}

View File

@ -3,7 +3,7 @@ package common
import (
"encoding/base64"
"encoding/binary"
"errors"
"github.com/matchsystems/werr"
)
const (
@ -11,6 +11,7 @@ const (
EncodingRawStd = "encoring_row_std"
)
// GetEncoding Returns base64 encoding based on the provided string
func GetEncoding(encoding string) (*base64.Encoding, error) {
switch encoding {
case EncodingRawUrl:
@ -18,12 +19,11 @@ func GetEncoding(encoding string) (*base64.Encoding, error) {
case EncodingRawStd:
return base64.RawStdEncoding, nil
default:
return nil, errors.New("unsupported encoding")
return nil, werr.Wrapf(ErrInvalidArgument, "unknown encoding %s", encoding)
}
}
// float64ToBytes converts float64 to []byte
// May be moved to nettools later
func float64ToBytes(f float64) []byte {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(f))

34
common/get_txs.go Normal file
View File

@ -0,0 +1,34 @@
package common
import "encoding/json"
const ()
type GetTxOpts struct {
TxHash []byte `param:"tx_hash"`
UserPubKey []byte `param:"user_pub_key"`
GetSent bool `param:"get_sent"`
GetReceived bool `param:"get_received"`
OrderAsc bool `param:"order_asc"`
Limit int `param:"limit"`
Offset int `param:"offset"`
Before int64 `param:"before"`
After int64 `param:"after"`
ByteEncoding string `param:"byte_encoding"`
}
type GetTxRespJson struct {
TXs []TxJson `json:"txs"`
PendingTXs []TxJson `json:"pending_txs"`
Count int `json:"count"`
ByteEncoding string `json:"byte_encoding"`
}
type GetTxResp struct {
TXs []Tx
PendingTXs []Tx
}
func (resp *GetTxResp) ToJson() ([]byte, error) {
return
}

5
common/node_info.go Normal file
View File

@ -0,0 +1,5 @@
package common
type NodeInfo struct {
NodeVersion string `json:"node_version"`
}

View File

@ -23,10 +23,6 @@ func GetTxHash(receiverPubKey []byte, message string, amount float64, blockId, t
return hash.Sum(nil)
}
func GetBlockId(t time.Time) int64 {
return (t.UTC().Unix() / 100) * 100
}
type NewTxOptsJson struct {
Hash string `json:"hash"` // use function GetTxHash
// (CreatedAt / 100) * 100
@ -103,7 +99,7 @@ func MakeNewTxOpts(senderPubKey, senderPrivateKey, receiverPubKey []byte,
now := time.Now().UTC()
createdAt := now.Unix()
createdAtNs := now.UnixNano()
blockId := (createdAt / 100) * 100
blockId := (createdAt / BlockSecsDiff) * BlockSecsDiff
txHash := GetTxHash(receiverPubKey, msg, amount, blockId, createdAt, createdAtNs)
sig := ed25519.Sign(senderPrivateKey, txHash)
return &NewTxOpts{
@ -121,7 +117,7 @@ func MakeNewTxOpts(senderPubKey, senderPrivateKey, receiverPubKey []byte,
}
func (opts *NewTxOpts) ValidateBlockId() (valid bool) {
return opts.BlockId == (opts.CreatedAt/100)*100
return opts.BlockId == (opts.CreatedAt/BlockSecsDiff)*BlockSecsDiff
}
func (opts *NewTxOpts) ValidateHash() (valid bool) {
return bytes.Equal(opts.Hash, GetTxHash(
@ -131,17 +127,40 @@ func (opts *NewTxOpts) ValidateHash() (valid bool) {
func (opts *NewTxOpts) ValidateSignature() (valid bool) {
return ed25519.Verify(opts.SenderPublicKey, opts.Hash, opts.Signature)
}
func (opts *Tx) ValidateBlockId() (valid bool) {
return opts.BlockId == (opts.CreatedAt/100)*100
func (opts *NewTxOpts) Validate() (err error) {
switch {
case !opts.ValidateBlockId():
return werr.Wrapf(ErrInvalidArgument, "invalid block id")
case !opts.ValidateHash():
return werr.Wrapf(ErrInvalidArgument, "invalid hash")
case !opts.ValidateSignature():
return werr.Wrapf(ErrInvalidArgument, "invalid signature")
}
return nil
}
func (opts *Tx) ValidateHash() (valid bool) {
return bytes.Equal(opts.Hash, GetTxHash(
opts.ReceiverPublicKey, opts.Message, opts.Amount, opts.BlockId, opts.CreatedAt, opts.CreatedAtNs,
func (tx *Tx) ValidateBlockId() (valid bool) {
return tx.BlockId == (tx.CreatedAt/BlockSecsDiff)*BlockSecsDiff
}
func (tx *Tx) ValidateHash() (valid bool) {
return bytes.Equal(tx.Hash, GetTxHash(
tx.ReceiverPublicKey, tx.Message, tx.Amount, tx.BlockId, tx.CreatedAt, tx.CreatedAtNs,
))
}
func (opts *Tx) ValidateSignature() (valid bool) {
return ed25519.Verify(opts.SenderPublicKey, opts.Hash, opts.Signature)
func (tx *Tx) ValidateSignature() (valid bool) {
return ed25519.Verify(tx.SenderPublicKey, tx.Hash, tx.Signature)
}
func (tx *Tx) Validate() (err error) {
if tx.ValidateBlockId() {
return werr.Wrapf(ErrInvalidArgument, "invalid block id")
} else if tx.ValidateHash() {
return werr.Wrapf(ErrInvalidArgument, "invalid hash")
} else if tx.ValidateSignature() {
return werr.Wrapf(ErrInvalidArgument, "invalid signature")
} else if ValidateBurningBool(tx) {
return werr.Wrapf(ErrInvalidArgument, "invalid burning")
}
return nil
}
// HashString uses base64.RawURLEncoding encoding
@ -155,19 +174,19 @@ func (opts *NewTxOpts) SignatureString() string {
}
// HashString uses base64.RawURLEncoding encoding
func (opts *Tx) HashString() string {
return base64.RawURLEncoding.EncodeToString(opts.Hash)
func (tx *Tx) HashString() string {
return base64.RawURLEncoding.EncodeToString(tx.Hash)
}
// SignatureString uses base64.RawURLEncoding encoding
func (opts *Tx) SignatureString() string {
return base64.RawURLEncoding.EncodeToString(opts.Signature)
func (tx *Tx) SignatureString() string {
return base64.RawURLEncoding.EncodeToString(tx.Signature)
}
func (opts *NewTxOpts) ToJSON(byteEncodingStr string) ([]byte, error) {
encoding, err := GetEncoding(byteEncodingStr)
if err != nil {
return nil, werr.Wrapf(err, "error while trying to get byte->string encoding")
return nil, werr.Wrapf(err, "error while getting byte->string encoding")
}
data := &NewTxOptsJson{
Hash: encoding.EncodeToString(opts.Hash),
@ -184,36 +203,36 @@ func (opts *NewTxOpts) ToJSON(byteEncodingStr string) ([]byte, error) {
}
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, werr.Wrapf(err, "error while trying to marshal to json")
return nil, werr.Wrapf(err, "error while marshaling")
}
return jsonBytes, nil
}
func (opts *Tx) ToJSON(byteEncodingStr string) ([]byte, error) {
func (tx *Tx) ToJSON(byteEncodingStr string) ([]byte, error) {
encoding, err := GetEncoding(byteEncodingStr)
if err != nil {
return nil, werr.Wrapf(err, "error while trying to get byte->string encoding")
return nil, werr.Wrapf(err, "error while getting byte->string encoding")
}
data := &TxJson{
Hash: encoding.EncodeToString(opts.Hash),
BlockId: opts.BlockId,
SenderPublicKey: encoding.EncodeToString(opts.SenderPublicKey),
ReceiverPublicKey: encoding.EncodeToString(opts.ReceiverPublicKey),
IsReward: opts.IsReward,
Amount: opts.Amount,
AmountBurned: opts.AmountBurned,
Message: opts.Message,
Signature: encoding.EncodeToString(opts.Signature),
CreatedAt: opts.CreatedAt,
CreatedAtNs: opts.CreatedAtNs,
AddedAt: opts.AddedAt,
IsAdded: opts.IsAdded,
Hash: encoding.EncodeToString(tx.Hash),
BlockId: tx.BlockId,
SenderPublicKey: encoding.EncodeToString(tx.SenderPublicKey),
ReceiverPublicKey: encoding.EncodeToString(tx.ReceiverPublicKey),
IsReward: tx.IsReward,
Amount: tx.Amount,
AmountBurned: tx.AmountBurned,
Message: tx.Message,
Signature: encoding.EncodeToString(tx.Signature),
CreatedAt: tx.CreatedAt,
CreatedAtNs: tx.CreatedAtNs,
AddedAt: tx.AddedAt,
IsAdded: tx.IsAdded,
ByteEncoding: byteEncodingStr,
}
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, werr.Wrapf(err, "error while trying to marshal to json")
return nil, werr.Wrapf(err, "error while marshaling")
}
return jsonBytes, nil
}
@ -229,19 +248,19 @@ func (opts *NewTxOptsJson) Process() (*NewTxOpts, error) {
}
encoding, err := GetEncoding(opts.ByteEncoding)
if err != nil {
return nil, werr.Wrapf(err, "error while trying to get byte->string encoding")
return nil, werr.Wrapf(err, "error while getting byte->string encoding")
}
if out.Hash, err = encoding.DecodeString(opts.Hash); err != nil {
return nil, werr.Wrapf(err, "error while trying to decode hash")
return nil, werr.Wrapf(err, "error while decoding hash")
}
if out.SenderPublicKey, err = encoding.DecodeString(opts.SenderPublicKey); err != nil {
return nil, werr.Wrapf(err, "error while trying to decode sender public key")
return nil, werr.Wrapf(err, "error while decoding sender public key")
}
if out.ReceiverPublicKey, err = encoding.DecodeString(opts.ReceiverPublicKey); err != nil {
return nil, werr.Wrapf(err, "error while trying to decode receiver public key")
return nil, werr.Wrapf(err, "error while decoding receiver public key")
}
if out.Signature, err = encoding.DecodeString(opts.Signature); err != nil {
return nil, werr.Wrapf(err, "error while trying to decode signature")
return nil, werr.Wrapf(err, "error while decoding signature")
}
return out, nil
}
@ -279,19 +298,19 @@ func (tx *TxJson) Process() (*Tx, error) {
}
encoding, err := GetEncoding(tx.ByteEncoding)
if err != nil {
return nil, werr.Wrapf(err, "error while trying to get byte->string encoding")
return nil, err
}
if out.Hash, err = encoding.DecodeString(tx.Hash); err != nil {
return nil, werr.Wrapf(err, "error while trying to decode hash")
return nil, werr.Wrapf(err, "error while decoding hash")
}
if out.SenderPublicKey, err = encoding.DecodeString(tx.SenderPublicKey); err != nil {
return nil, werr.Wrapf(err, "error while trying to decode sender public key")
return nil, werr.Wrapf(err, "error while decoding sender public key")
}
if out.ReceiverPublicKey, err = encoding.DecodeString(tx.ReceiverPublicKey); err != nil {
return nil, werr.Wrapf(err, "error while trying to decode receiver public key")
return nil, werr.Wrapf(err, "error while decoding receiver public key")
}
if out.Signature, err = encoding.DecodeString(tx.Signature); err != nil {
return nil, werr.Wrapf(err, "error while trying to decode signature")
return nil, werr.Wrapf(err, "error while decoding signature")
}
return out, nil
}
@ -299,14 +318,14 @@ func (tx *TxJson) Process() (*Tx, error) {
func NewTxOptsFromJSON(marshaledData []byte) (opts *NewTxOpts, err error) {
data := &NewTxOptsJson{}
if err := json.Unmarshal(marshaledData, data); err != nil {
return nil, werr.Wrapf(err, "error while trying to unmarshal new transaction options")
return nil, werr.Wrapf(err, "error while unmarshaling")
}
return data.Process()
}
func NewTxOptsFromJsonReader(r io.Reader) (opts *NewTxOpts, err error) {
data := &NewTxOptsJson{}
if err := json.NewDecoder(r).Decode(&data); err != nil {
return nil, werr.Wrapf(err, "error while trying to unmarshal new transaction options")
return nil, werr.Wrapf(err, "error while unmarshaling")
}
return data.Process()
}
@ -314,14 +333,14 @@ func NewTxOptsFromJsonReader(r io.Reader) (opts *NewTxOpts, err error) {
func TxFromJSON(marshaledData []byte) (opts *Tx, err error) {
data := &TxJson{}
if err := json.Unmarshal(marshaledData, data); err != nil {
return nil, werr.Wrapf(err, "error while trying to unmarshal new transaction options")
return nil, werr.Wrapf(err, "error while unmarshaling")
}
return data.Process()
}
func TxFromJsonReader(r io.Reader) (opts *Tx, err error) {
data := &TxJson{}
if err := json.NewDecoder(r).Decode(&data); err != nil {
return nil, werr.Wrapf(err, "error while trying to unmarshal new transaction options")
return nil, werr.Wrapf(err, "error while unmarshaling")
}
return data.Process()
}

8
infra.docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
services:
postgres:
image: postgres:latest
ports:
- "127.0.0.1:5432:5432"
volumes:
- ./pg_data:/var/lib/postgres
environment:

View File

@ -2,15 +2,47 @@ package http
import (
"context"
"encoding/json"
"mic-wallet/common"
"mic-wallet/server/core"
"net/http"
)
func NewApi(ctx context.Context, baseRoute string) *http.ServeMux {
type ApiDependencies struct {
*core.BlocksCreator
}
/*
/ -- node info
/txs -- add/get txs
/blocks -- get/blocks
/blocks/current -- get current block info
*/
func NewApi(ctx context.Context, d ApiDependencies, baseRoute string) *http.ServeMux {
if baseRoute == "" {
baseRoute = "/"
}
mux := http.NewServeMux()
/*
*/
mux.HandleFunc(baseRoute, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
w.WriteHeader(http.StatusNotFound)
return
}
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
data, _ := json.Marshal(common.NodeInfo{
NodeVersion: common.VersionNumberStr,
})
w.Write(data)
w.WriteHeader(http.StatusOK)
})
return mux
}

View File

@ -3,20 +3,35 @@ package http
import (
"context"
"encoding/json"
"errors"
"github.com/matchsystems/werr"
"log"
"mic-wallet/common"
"mic-wallet/server/core"
logicErr "mic-wallet/server/logic/err"
"net/http"
)
type NewTransactionReqBody struct {
}
func NewTransaction(w http.ResponseWriter, r *http.Request) *logicErr.Err {
req := &NewTransactionReqBody{}
func NewTransaction(ctx context.Context, w http.ResponseWriter, r *http.Request, d ApiDependencies) {
defer func() {
if err := r.Body.Close(); err != nil {
log.Printf("error closing request body: %s", err.Error())
}
}()
req := &common.NewTxOpts{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if err := r.Body.Close(); err != nil {
if err := d.WriteTx(req); err != nil {
if errors.Is(werr.UnwrapAll(err), core.ErrTxDuplicate) {
http.Error(w, "transaction already exists", http.StatusConflict)
return
}
w.WriteHeader(http.StatusInternalServerError)
}
return nil
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func GetTransaction(w http.ResponseWriter, r *http.Request) *logicErr.Err {

View File

@ -2,25 +2,33 @@ package core
import (
"crypto/sha512"
"fmt"
"encoding/binary"
"github.com/matchsystems/werr"
"mic-wallet/common"
"mic-wallet/server/repository/entities"
"sync"
)
type Block struct {
ID int64
Id int64
PrevHash []byte
Hash []byte
TXs map[string]*common.NewTxOpts
// If there is some transactions added after block is finished
TXsLeftOver map[string]*common.NewTxOpts
IgnoreSignatures map[string]struct{}
TXs map[string]*common.NewTxOpts
Lock *sync.RWMutex
PrevHash []byte
Hash []byte
IsFinished bool
}
func NewBlock(id int64, prevHash []byte) *Block {
func NewBlock(id int64, prevHash []byte,
prevTxsLeftOver map[string]*common.NewTxOpts, txsLeftOver map[string]*common.NewTxOpts) *Block {
return &Block{
ID: id,
Id: id,
IgnoreSignatures: make(map[string]struct{}),
TXs: make(map[string]*common.NewTxOpts),
TXs: prevTxsLeftOver,
TXsLeftOver: txsLeftOver,
Lock: &sync.RWMutex{},
PrevHash: prevHash,
}
@ -28,25 +36,41 @@ func NewBlock(id int64, prevHash []byte) *Block {
func (b *Block) AddTx(tx *common.NewTxOpts) error {
b.Lock.Lock()
hashStr := tx.HashString()
sigStr := tx.SignatureString()
hashStr := tx.HashString()
if _, ok := b.IgnoreSignatures[sigStr]; ok {
return fmt.Errorf("duplicate")
return werr.Wrapf(common.ErrEntityExists,
"transaction with signature %s already exists", sigStr)
}
if !b.IsFinished {
b.TXs[hashStr] = tx
} else {
b.TXsLeftOver[hashStr] = tx
}
b.TXs[hashStr] = tx
b.Lock.Unlock()
return nil
}
func (b *Block) RemoveTx(hash string) {}
func (b *Block) CalcHash() []byte {
func (b *Block) Finish() ([]byte, error) {
hash := sha512.New()
if err := binary.Write(hash, binary.BigEndian, b.Id); err != nil {
return nil, werr.Wrapf(err, "failed to write block id into hash buffer for block %d", b.Id)
}
b.Lock.Lock()
hash.Write(b.PrevHash)
for _, tx := range b.TXs {
hash.Write(tx.Hash)
}
b.IsFinished = true
b.Lock.Unlock()
b.Hash = hash.Sum(nil)
return b.Hash
return b.Hash, nil
}
func (b *Block) ToRepoEntity() *entities.Block {
return &entities.Block{
Id: b.Id,
Hash: b.Hash,
PrevHash: b.PrevHash,
}
}

View File

@ -19,8 +19,8 @@ type IBlocksCommiter interface {
type BlocksCommiter struct {
BlocksQueue []*Block
Lock *sync.RWMutex
CountLim int32
CountBusy int32
CountLim int
CountBusy int
TaskDeadline time.Duration
ErrChan chan error
@ -31,16 +31,19 @@ func NewBlocksCommiter(repo *repository.Repository, errChan chan error, workersC
return &BlocksCommiter{
BlocksQueue: make([]*Block, 0),
Lock: &sync.RWMutex{},
CountLim: 0,
CountLim: workersCountLim,
CountBusy: 0,
TaskDeadline: 0,
ErrChan: make(chan error, workersCountLim),
ErrChan: errChan,
CommitBlockFunc: func(ctx context.Context, block *Block) error {
return CommitBlock(ctx, repo, block)
},
}
}
// execute executes commiting block with deadline and cancelable by the context
// provided when calling CommitBlock.
// all the errors from it (including context) are sent to BlocksCommiter.ErrChan
func (bc *BlocksCommiter) execute(ctx context.Context, block *Block) {
if bc.TaskDeadline > 0 {
var cancel context.CancelFunc
@ -55,9 +58,9 @@ func (bc *BlocksCommiter) execute(ctx context.Context, block *Block) {
}()
select {
case <-ctx.Done():
errChan <- werr.Wrapf(ctx.Err(), "ctx error while commiting block %d", block.ID)
bc.ErrChan <- werr.Wrapf(ctx.Err(), "ctx error while commiting block %d", block.Id)
case err := <-errChan:
bc.ErrChan <- werr.Wrapf(err, "error while commiting block %d", block.ID)
bc.ErrChan <- werr.Wrapf(err, "error while commiting block %d", block.Id)
}
}

View File

@ -0,0 +1,7 @@
package core
import "testing"
func TestBlocksCommiter_CommitBlock(t *testing.T) {
// TODO
}

View File

@ -8,6 +8,9 @@ import (
"time"
)
var ErrOutOfDateBlockId = errors.New("")
var ErrBlockIdDoesntExist = errors.New("")
type IBlocksCreator interface {
Run(ctx context.Context) error
WriteTx(opts *common.NewTxOpts) error
@ -16,41 +19,69 @@ type IBlocksCreator interface {
GetTxs() []*common.NewTxOpts
}
// BlocksCreator creates and rotates new Blocks and pushed them to the commiter
type BlocksCreator struct {
blocks map[int64]*Block
txsLeftOver map[string]*common.NewTxOpts
commiter IBlocksCommiter
prevBlockHash []byte
}
func NewBlocksCreator(commiter IBlocksCommiter) *BlocksCreator {
return &BlocksCreator{
blocks: make(map[int64]*Block),
commiter: commiter,
blocks: make(map[int64]*Block),
txsLeftOver: make(map[string]*common.NewTxOpts),
commiter: commiter,
}
}
func (bc *BlocksCreator) Run(ctx context.Context) error {
func (bc *BlocksCreator) Run(ctx context.Context) (err error) {
var blockID int64
var prevLeftOver map[string]*common.NewTxOpts
for {
blockID = common.GetBlockId(time.Now())
if oldBlock, ok := bc.blocks[blockID-100]; ok { // Commit and delete old block
bc.prevBlockHash = oldBlock.CalcHash()
bc.commiter.CommitBlock(ctx, oldBlock)
delete(bc.blocks, blockID-100)
if oldBlock, ok := bc.blocks[blockID-common.BlockSecsDiff]; ok { // Commit and delete old block
// we need to know previous hash for new block.
// new transactions while oldBlock.Finish is executing will
// end up in leftOver so they will be parts of the next block
if bc.prevBlockHash, err = oldBlock.Finish(); err != nil {
return nil
}
go bc.commiter.CommitBlock(ctx, oldBlock) // may take a while
delete(bc.blocks, blockID-common.BlockSecsDiff)
}
bc.blocks[blockID] = NewBlock(blockID, bc.prevBlockHash)
time.Sleep(time.Until(time.Unix(blockID+100, 0)))
// prevLeftOver becomes new block's transactions
// so there is no "lost" transactions
prevLeftOver = bc.txsLeftOver
// and also now we need to make new leftOver
bc.txsLeftOver = make(map[string]*common.NewTxOpts)
bc.blocks[blockID] = NewBlock(blockID, bc.prevBlockHash, prevLeftOver, bc.txsLeftOver)
// Waiting for the next block
time.Sleep(time.Until(time.Unix(blockID+common.BlockSecsDiff, 0)))
}
}
// WriteTx validates transaction and writes it into the current block
// if transaction is valid
func (bc *BlocksCreator) WriteTx(opts *common.NewTxOpts) error {
// validating may take some time, so to don't write into
if err := opts.Validate(); err != nil {
return werr.Wrapf(err,
"error validating transaction %s", opts.HashString())
}
currentBlockID := common.GetBlockId(time.Now())
if currentBlockID < opts.BlockId {
return errors.New("block with such id wasn't created yet. " +
"Ensure that you generate block IDs in UTC timezone")
return werr.Wrapf(common.ErrEntityNotFound, "block id %d does not exist", currentBlockID)
} else if currentBlockID > opts.BlockId {
return errors.New("block id is out of date")
return werr.Wrapf(common.ErrEntityOutdated, "block id %d is out of date", currentBlockID)
}
return werr.Wrapf(bc.blocks[currentBlockID].AddTx(opts),
"error adding transaction to block %d", currentBlockID)
block, ok := bc.blocks[currentBlockID]
if !ok {
return werr.Wrapf(common.ErrEntityNotFound, "block id %d does not exist", currentBlockID)
}
if err := block.AddTx(opts); err != nil {
return werr.Wrapf(err,
"error adding transaction to block %d", currentBlockID)
}
return nil
}

View File

@ -2,9 +2,46 @@ package core
import (
"context"
"github.com/matchsystems/werr"
"mic-wallet/common"
"mic-wallet/server/repository"
"mic-wallet/server/repository/entities"
)
func CommitBlock(ctx context.Context, repo *repository.Repository, block *Block) error {
return nil
func NewTxOptsToRepoEntity(opts *common.NewTxOpts) *entities.NewTransactionOpts {
return &entities.NewTransactionOpts{
Hash: opts.Hash,
BlockId: opts.BlockId,
SenderPublicKey: opts.SenderPublicKey,
ReceiverPublicKey: opts.ReceiverPublicKey,
IsReward: opts.IsReward,
Amount: opts.Amount,
AmountBurned: common.CalcBurning(opts.Amount),
Message: opts.Message,
Signature: opts.Signature,
CreatedAt: opts.CreatedAt,
CreatedAtNs: opts.CreatedAtNs,
}
}
func CommitBlock(ctx context.Context, repo *repository.Repository, block *Block) (err error) {
// If micw will be decentralized
// this function will be sending blocks to other nodes
// and then there will be block-fetcher to see which blocks were accepted to the network
if err = repo.AddBLock(ctx, &entities.Block{
Id: block.Id,
Hash: block.Hash,
PrevHash: block.PrevHash,
}); err != nil {
return werr.Wrapf(err, "failed to commit block")
}
repoTxOpts := make([]*entities.NewTransactionOpts, len(block.TXs))
var i int
for _, tx := range block.TXs {
repoTxOpts[i] = NewTxOptsToRepoEntity(tx)
i++
}
return werr.Wrapf(repo.AddTransactions(ctx, repoTxOpts), "failed to commit transactions")
}

View File

@ -11,7 +11,6 @@ CREATE TABLE blocks (
hash BYTEA UNIQUE,
-- NULL only on the first block
prev_hash BYTEA UNIQUE,
finished_at TIMESTAMP,
)
CREATE TABLE IF NOT EXISTS transactions (

View File

@ -9,8 +9,7 @@ type (
Id int64 `db:"id"`
Hash []byte `db:"hash"`
PrevHash []byte `db:"prev_hash"`
StartedAt time.Time `db:"started_at"`
FinishedAt *time.Time `db:"finished_at"`
FinishedAt time.Time `db:"finished_at"`
Transactions []Transaction `db:"transactions"`
}
@ -18,23 +17,8 @@ type (
Id int64 `db:"id"`
Hash []byte `db:"hash"`
PrevHash []byte `db:"prev_hash"`
StartedAt time.Time `db:"started_at"`
FinishedAt *time.Time `db:"finished_at"`
}
NewBlockOpts struct {
Id int64 `db:"id"`
Hash []byte `db:"hash"` // May be nil
PrevHash []byte `db:"prev_hash"` // May be nil
StartedAt time.Time `db:"started_at"`
FinishedAt *time.Time `db:"finished_at"` // May be nil
}
BlockFinishedOpts struct {
Id int64 `db:"id"`
Hash []byte `db:"hash"`
FinishedAt time.Time `db:"finished_at"`
}
)
type (
@ -54,16 +38,16 @@ type (
}
NewTransactionOpts struct {
Hash []byte `db:"hash"`
BlockId int64 `db:"block_id"`
SenderPublicKey []byte `db:"sender_public_key"`
ReceiverPublicKey []byte `db:"receiver_public_key"`
IsReward bool `db:"is_reward"`
Amount float64 `db:"amount"`
AmountBurned float64 `db:"amount_burned"`
Message string `db:"message"`
Signature []byte `db:"signature"`
CreatedAt time.Time `db:"created_at"`
CreatedAtNs int64 `db:"created_at_ns"`
Hash []byte `db:"hash"`
BlockId int64 `db:"block_id"`
SenderPublicKey []byte `db:"sender_public_key"`
ReceiverPublicKey []byte `db:"receiver_public_key"`
IsReward bool `db:"is_reward"`
Amount float64 `db:"amount"`
AmountBurned float64 `db:"amount_burned"`
Message string `db:"message"`
Signature []byte `db:"signature"`
CreatedAt int64 `db:"created_at"`
CreatedAtNs int64 `db:"created_at_ns"`
}
)

View File

@ -7,25 +7,28 @@ import (
)
type IBlocksRepository interface {
NewBLock(ctx context.Context, opts e.NewBlockOpts) (blockID int64, err error)
FinishBlock(ctx context.Context, opts e.BlockFinishedOpts) (err error)
AddBLock(ctx context.Context, opts *e.Block) (err error)
GetBlock(ctx context.Context, id int64) (block *e.Block, err error)
GetBlockByHash(ctx context.Context, hash string) (block *e.Block, err error)
GetLastFinishedBlock(ctx context.Context) (block *e.Block, err error)
GetBlockWithTxs(ctx context.Context, id int64) (block *e.BlockWithTransactions, err error)
GetBlockWithTxsByHash(ctx context.Context, hash string) (block *e.BlockWithTransactions, err error)
}
type ITransactionsRepository interface {
GetLastTransactionHash(ctx context.Context) ([]byte, error)
AddTransaction(ctx context.Context, opts e.NewTransactionOpts) (transaction *e.Transaction, err error)
GetTransaction(ctx context.Context, txID int64) (tx e.Transaction, err error)
AddTransaction(ctx context.Context, opts *e.NewTransactionOpts) (transaction *e.Transaction, err error)
AddTransactions(ctx context.Context, opts []*e.NewTransactionOpts) (err error)
GetTransaction(ctx context.Context, hash string) (tx *e.Transaction, err error)
GetUserIncomingTransactions(
ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]e.Transaction, error)
ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]*e.Transaction, error)
GetUserOutcomingTransactions(
ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]e.Transaction, error)
ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]*e.Transaction, error)
GetUserTransactions(
ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]e.Transaction, error)
ctx context.Context, userPubKey string, orderAsc bool, limit int, offset int) ([]*e.Transaction, error)
GetTransactions(
ctx context.Context, orderAsc bool, limit int, offset int) ([]e.Transaction, error)
ctx context.Context, orderAsc bool, limit int, offset int) ([]*e.Transaction, error)
}
type Repository struct {

View File

@ -3,34 +3,36 @@ package server
import (
"context"
"fmt"
commonUtils "git.mic.pp.ua/anderson/nettools/common"
"net/http"
)
func (s *Server) Run(ctx context.Context) error {
tasks := make([]commonUtils.Task, 0)
s.ErrChan = make(chan error)
defer close(s.ErrChan)
tasks := make([]serverTask, 0)
var mux *http.ServeMux
if s.HttpListener != nil || s.HttpsListener != nil {
mux = http.NewServeMux()
}
if s.HttpListener != nil {
tasks = append(tasks, func(ctx context.Context) error {
return http.Serve(s.HttpListener, mux)
tasks = append(tasks, func() error {
return s.RunHttp(mux)
})
}
if s.HttpsListener != nil {
tasks = append(tasks, func(ctx context.Context) error {
return http.ServeTLS(s.HttpsListener, mux, s.Cfg.TlsCfg.Cert, s.Cfg.TlsCfg.Key)
tasks = append(tasks, func() error {
return s.RunHttps(mux)
})
}
if s.GrpcListener != nil {
tasks = append(tasks, func(ctx context.Context) error {
return nil
tasks = append(tasks, func() error {
return s.RunGrpc()
})
}
return commonUtils.ExecTasks(ctx, 0, tasks)
return s.execTasks(ctx, tasks)
}
func (s *Server) RunHttp(mux http.Handler) error {

View File

@ -2,38 +2,45 @@ package server
import (
"context"
commonUtils "git.mic.pp.ua/anderson/nettools/common"
"github.com/jackc/pgx/v5/pgxpool"
"mic-wallet/server/config"
"mic-wallet/server/core"
"mic-wallet/server/repository"
"net"
"net/http"
)
type serverTask func() error
type Server struct {
Cfg *config.Processed
DB *pgxpool.Pool
HttpListener net.Listener
HttpsListener net.Listener
GrpcListener net.Listener
Mux *http.ServeMux
Repo *repository.Repository
Cfg *config.Processed
DB *pgxpool.Pool
Repo *repository.Repository
BlocksCommiter *core.BlocksCommiter
BlocksCreator *core.BlocksCreator
HttpListener net.Listener
HttpsListener net.Listener
GrpcListener net.Listener
Mux *http.ServeMux
ErrChan chan error
}
func New(cfg *config.Processed) *Server {
return &Server{Cfg: cfg}
return &Server{
Cfg: cfg,
ErrChan: make(chan error),
}
}
func (s *Server) execTasks(ctx context.Context, tasks []commonUtils.Task) error {
errChan := make(chan error)
func (s *Server) execTasks(ctx context.Context, tasks []serverTask) error {
for _, task := range tasks {
go func() {
errChan <- task(ctx)
s.ErrChan <- task()
}()
}
for {
select {
case err := <-errChan:
case err := <-s.ErrChan:
if err != nil {
return err
}

View File

@ -11,16 +11,15 @@ import (
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/matchsystems/werr"
"log"
"mic-wallet/server/core"
"mic-wallet/server/repository"
"net"
"net/http"
"runtime"
"time"
)
func (s *Server) Setup(ctx context.Context) error {
// ================================
// Set up the transactions pipeline
// ===================
// Setting up database
@ -67,7 +66,13 @@ func (s *Server) Setup(ctx context.Context) error {
// Internals initialization
s.Repo = repository.NewRepository(s.DB)
// TODO:
commiterWorkersLim := runtime.NumCPU() - 1
if commiterWorkersLim < 1 {
commiterWorkersLim = 1
}
s.BlocksCommiter = core.NewBlocksCommiter(s.Repo, s.ErrChan, commiterWorkersLim)
s.BlocksCreator = core.NewBlocksCreator(s.BlocksCommiter)
// ======================
// Creating net listeners
@ -79,7 +84,7 @@ func (s *Server) Setup(ctx context.Context) error {
addr := fmt.Sprintf("%s:%d", s.Cfg.Addr, s.Cfg.HttpPort)
s.HttpListener, err = net.Listen("tcp", addr)
if err != nil {
return werr.Wrapf(err, "failed to listen http_api_entities on address %s", addr)
return werr.Wrapf(err, "failed to listen http on address %s", addr)
}
}
if s.Cfg.HttpsPort > 0 {