From 4e044cdad95171a59408b36821bca5ea3cc449fd Mon Sep 17 00:00:00 2001 From: Dmitry Anderson <4nd3r5z0n@gmail.com> Date: Sun, 17 Nov 2024 22:24:19 +0100 Subject: [PATCH] common and core changed --- common/block.go | 10 + common/burning.go | 9 + common/common.go | 31 +++ common/consts.go | 11 - common/http_err.go | 4 + common/signatue.go | 52 ---- common/tx.go | 327 +++++++++++++++++++++++++ common/tx_test.go | 74 ++++++ common/version.go | 7 + server/core/block.go | 52 ++++ server/core/block_pipeline.go | 47 ---- server/core/blocks_commiter.go | 84 +++++++ server/core/blocks_creator.go | 56 +++++ server/core/commit_block.go | 10 + server/core/entities.go | 15 -- server/logic/validate.go | 31 --- server/migrations/0.up.sql | 36 +-- server/repository/entities/entities.go | 2 + server/setup.go | 6 +- 19 files changed, 688 insertions(+), 176 deletions(-) create mode 100644 common/block.go create mode 100644 common/burning.go create mode 100644 common/common.go delete mode 100644 common/consts.go create mode 100644 common/http_err.go delete mode 100644 common/signatue.go create mode 100644 common/tx.go create mode 100644 common/tx_test.go create mode 100644 common/version.go create mode 100644 server/core/block.go delete mode 100644 server/core/block_pipeline.go create mode 100644 server/core/blocks_commiter.go create mode 100644 server/core/blocks_creator.go create mode 100644 server/core/commit_block.go delete mode 100644 server/core/entities.go delete mode 100644 server/logic/validate.go diff --git a/common/block.go b/common/block.go new file mode 100644 index 0000000..33321d1 --- /dev/null +++ b/common/block.go @@ -0,0 +1,10 @@ +package common + +type BlockJson struct { +} + +type Block struct { + Id int64 + PrevHash []byte + Hash []byte +} diff --git a/common/burning.go b/common/burning.go new file mode 100644 index 0000000..e058658 --- /dev/null +++ b/common/burning.go @@ -0,0 +1,9 @@ +package common + +import "math" + +const BurningRate = 0.001 + +func CalcBurning(trxAmount float64) float64 { + return BurningRate * math.Sqrt(trxAmount) +} diff --git a/common/common.go b/common/common.go new file mode 100644 index 0000000..f84a5d3 --- /dev/null +++ b/common/common.go @@ -0,0 +1,31 @@ +package common + +import ( + "encoding/base64" + "encoding/binary" + "errors" +) + +const ( + EncodingRawUrl = "encoring_row_url" + EncodingRawStd = "encoring_row_std" +) + +func GetEncoding(encoding string) (*base64.Encoding, error) { + switch encoding { + case EncodingRawUrl: + return base64.RawURLEncoding, nil + case EncodingRawStd: + return base64.RawStdEncoding, nil + default: + return nil, errors.New("unsupported 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)) + return buf +} diff --git a/common/consts.go b/common/consts.go deleted file mode 100644 index ca98c0a..0000000 --- a/common/consts.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -const VersionNumber = 0.01 -const MinBurningAmount = 0.001 - -func CalcBurning(trxAmount float64) float64 { - if trxAmount < 1 { - return MinBurningAmount - } - return trxAmount * MinBurningAmount -} diff --git a/common/http_err.go b/common/http_err.go new file mode 100644 index 0000000..4dff4cd --- /dev/null +++ b/common/http_err.go @@ -0,0 +1,4 @@ +package common + +type HttpErr struct { +} diff --git a/common/signatue.go b/common/signatue.go deleted file mode 100644 index ba16cc2..0000000 --- a/common/signatue.go +++ /dev/null @@ -1,52 +0,0 @@ -package common - -import ( - "bytes" - ed "crypto/ed25519" - "crypto/sha512" - "encoding/binary" - "time" -) - -func Float64ToBytes(f float64) []byte { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, uint64(f)) - return buf -} - -func GetTransactionHash(receiverPubKey []byte, message string, amount float64, createdAt time.Time) []byte { - amountBytes := Float64ToBytes(amount) - hash := sha512.New() - timestamp := createdAt.Unix() - _ = binary.Write(hash, binary.BigEndian, timestamp) - hash.Write([]byte(message)) - hash.Write(receiverPubKey) - hash.Write(amountBytes) - return hash.Sum(nil) -} - -func getSignMsg(txHash []byte, createdAt time.Time) ([]byte, error) { - buf := bytes.NewBuffer(txHash) - timestamp := createdAt.Unix() - if err := binary.Write(buf, binary.BigEndian, timestamp); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func SingTransaction(key ed.PrivateKey, txHash []byte, signedAt time.Time) (sig []byte, err error) { - msg, err := getSignMsg(txHash, signedAt) - if err != nil { - return nil, err - } - sig = ed.Sign(key, msg) - return sig, nil -} - -func VerifyTransactionSignature(key ed.PublicKey, signature []byte, txHash []byte, signedAt time.Time) (valid bool, err error) { - msg, err := getSignMsg(txHash, signedAt) - if err != nil { - return false, err - } - return ed.Verify(key, msg, signature), nil -} diff --git a/common/tx.go b/common/tx.go new file mode 100644 index 0000000..296543d --- /dev/null +++ b/common/tx.go @@ -0,0 +1,327 @@ +package common + +import ( + "bytes" + "crypto/sha512" + "encoding/base64" + "encoding/binary" + "encoding/json" + "github.com/matchsystems/werr" + "golang.org/x/crypto/ed25519" + "io" + "time" +) + +func GetTxHash(receiverPubKey []byte, message string, amount float64, blockId, timestamp, timestampNs int64) []byte { + hash := sha512.New() + _ = binary.Write(hash, binary.BigEndian, blockId) + _ = binary.Write(hash, binary.BigEndian, timestamp) + _ = binary.Write(hash, binary.BigEndian, timestampNs) + hash.Write(float64ToBytes(amount)) + hash.Write([]byte(message)) + hash.Write(receiverPubKey) + 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 + BlockId int64 `json:"block_id"` + SenderPublicKey string `json:"sender_public_key"` + ReceiverPublicKey string `json:"receiver_public_key"` + IsReward bool `json:"is_reward"` + Amount float64 `json:"amount"` + Message string `json:"message"` + Signature string `json:"signature"` + CreatedAt int64 `json:"created_at"` + CreatedAtNs int64 `json:"created_at_ns"` + + ByteEncoding string `json:"byte_encoding"` +} + +// NewTxOpts sent from client to server to add a new transaction +type NewTxOpts struct { + Hash []byte + BlockId int64 + SenderPublicKey []byte + ReceiverPublicKey []byte + IsReward bool + Amount float64 + Message string + // Signed TX Hash with senders private key + Signature []byte + CreatedAt int64 + CreatedAtNs int64 // Nanoseconds +} + +type TxJson struct { + Hash string `json:"hash"` // use function GetTxHash + // (CreatedAt / 100) * 100 + BlockId int64 `json:"block_id"` + SenderPublicKey string `json:"sender_public_key"` + ReceiverPublicKey string `json:"receiver_public_key"` + IsReward bool `json:"is_reward"` + Amount float64 `json:"amount"` + AmountBurned float64 `json:"amount_burned"` + Message string `json:"message"` + Signature string `json:"signature"` + CreatedAt int64 `json:"created_at"` + CreatedAtNs int64 `json:"created_at_ns"` + AddedAt time.Time `json:"added_at"` + IsAdded bool `json:"is_added"` + + ByteEncoding string `json:"byte_encoding"` +} + +// Tx is a transaction structure +// that is sent from server to the client +// with the information about requested transaction +type Tx struct { + Hash []byte + BlockId int64 + SenderPublicKey []byte + ReceiverPublicKey []byte + IsReward bool + Amount float64 + AmountBurned float64 + Message string + // Signed TX Hash with senders private key + Signature []byte + CreatedAt int64 + CreatedAtNs int64 // Nanoseconds + AddedAt time.Time + IsAdded bool +} + +func MakeNewTxOpts(senderPubKey, senderPrivateKey, receiverPubKey []byte, + amount float64, msg string, isReward bool) *NewTxOpts { + + now := time.Now().UTC() + createdAt := now.Unix() + createdAtNs := now.UnixNano() + blockId := (createdAt / 100) * 100 + txHash := GetTxHash(receiverPubKey, msg, amount, blockId, createdAt, createdAtNs) + sig := ed25519.Sign(senderPrivateKey, txHash) + return &NewTxOpts{ + Hash: txHash, + BlockId: blockId, + SenderPublicKey: senderPubKey, + ReceiverPublicKey: receiverPubKey, + IsReward: isReward, + Amount: amount, + Message: msg, + Signature: sig, + CreatedAt: createdAt, + CreatedAtNs: createdAtNs, + } +} + +func (opts *NewTxOpts) ValidateBlockId() (valid bool) { + return opts.BlockId == (opts.CreatedAt/100)*100 +} +func (opts *NewTxOpts) ValidateHash() (valid bool) { + return bytes.Equal(opts.Hash, GetTxHash( + opts.ReceiverPublicKey, opts.Message, opts.Amount, opts.BlockId, opts.CreatedAt, opts.CreatedAtNs, + )) +} +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 *Tx) ValidateHash() (valid bool) { + return bytes.Equal(opts.Hash, GetTxHash( + opts.ReceiverPublicKey, opts.Message, opts.Amount, opts.BlockId, opts.CreatedAt, opts.CreatedAtNs, + )) +} +func (opts *Tx) ValidateSignature() (valid bool) { + return ed25519.Verify(opts.SenderPublicKey, opts.Hash, opts.Signature) +} + +// HashString uses base64.RawURLEncoding encoding +func (opts *NewTxOpts) HashString() string { + return base64.RawURLEncoding.EncodeToString(opts.Hash) +} + +// SignatureString uses base64.RawURLEncoding encoding +func (opts *NewTxOpts) SignatureString() string { + return base64.RawURLEncoding.EncodeToString(opts.Signature) +} + +// HashString uses base64.RawURLEncoding encoding +func (opts *Tx) HashString() string { + return base64.RawURLEncoding.EncodeToString(opts.Hash) +} + +// SignatureString uses base64.RawURLEncoding encoding +func (opts *Tx) SignatureString() string { + return base64.RawURLEncoding.EncodeToString(opts.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") + } + data := &NewTxOptsJson{ + Hash: encoding.EncodeToString(opts.Hash), + BlockId: opts.BlockId, + SenderPublicKey: encoding.EncodeToString(opts.SenderPublicKey), + ReceiverPublicKey: encoding.EncodeToString(opts.ReceiverPublicKey), + IsReward: opts.IsReward, + Amount: opts.Amount, + Message: opts.Message, + Signature: encoding.EncodeToString(opts.Signature), + CreatedAt: opts.CreatedAt, + CreatedAtNs: opts.CreatedAtNs, + ByteEncoding: byteEncodingStr, + } + jsonBytes, err := json.Marshal(data) + if err != nil { + return nil, werr.Wrapf(err, "error while trying to marshal to json") + } + return jsonBytes, nil +} + +func (opts *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") + } + 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, + + ByteEncoding: byteEncodingStr, + } + jsonBytes, err := json.Marshal(data) + if err != nil { + return nil, werr.Wrapf(err, "error while trying to marshal to json") + } + return jsonBytes, nil +} + +func (opts *NewTxOptsJson) Process() (*NewTxOpts, error) { + out := &NewTxOpts{ + BlockId: opts.BlockId, + IsReward: opts.IsReward, + Amount: opts.Amount, + Message: opts.Message, + CreatedAt: opts.CreatedAt, + CreatedAtNs: opts.CreatedAtNs, + } + encoding, err := GetEncoding(opts.ByteEncoding) + if err != nil { + return nil, werr.Wrapf(err, "error while trying to get byte->string encoding") + } + if out.Hash, err = encoding.DecodeString(opts.Hash); err != nil { + return nil, werr.Wrapf(err, "error while trying to decode hash") + } + if out.SenderPublicKey, err = encoding.DecodeString(opts.SenderPublicKey); err != nil { + return nil, werr.Wrapf(err, "error while trying to decode 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") + } + if out.Signature, err = encoding.DecodeString(opts.Signature); err != nil { + return nil, werr.Wrapf(err, "error while trying to decode signature") + } + return out, nil +} + +func (opts *NewTxOpts) Process(isAdded bool, addedAt time.Time) (*Tx, error) { + out := &Tx{ + Hash: opts.Hash, + BlockId: opts.BlockId, + SenderPublicKey: opts.SenderPublicKey, + ReceiverPublicKey: opts.ReceiverPublicKey, + IsReward: opts.IsReward, + Amount: opts.Amount, + AmountBurned: CalcBurning(opts.Amount), + Message: opts.Message, + Signature: opts.Signature, + CreatedAt: opts.CreatedAt, + CreatedAtNs: opts.CreatedAtNs, + AddedAt: addedAt, + IsAdded: isAdded, + } + return out, nil +} + +func (tx *TxJson) Process() (*Tx, error) { + out := &Tx{ + BlockId: tx.BlockId, + IsReward: tx.IsReward, + Amount: tx.Amount, + AmountBurned: tx.AmountBurned, + Message: tx.Message, + CreatedAt: tx.CreatedAt, + CreatedAtNs: tx.CreatedAtNs, + AddedAt: tx.AddedAt, + IsAdded: tx.IsAdded, + } + encoding, err := GetEncoding(tx.ByteEncoding) + if err != nil { + return nil, werr.Wrapf(err, "error while trying to get byte->string encoding") + } + if out.Hash, err = encoding.DecodeString(tx.Hash); err != nil { + return nil, werr.Wrapf(err, "error while trying to decode hash") + } + if out.SenderPublicKey, err = encoding.DecodeString(tx.SenderPublicKey); err != nil { + return nil, werr.Wrapf(err, "error while trying to decode 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") + } + if out.Signature, err = encoding.DecodeString(tx.Signature); err != nil { + return nil, werr.Wrapf(err, "error while trying to decode signature") + } + return out, nil +} + +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 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 data.Process() +} + +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 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 data.Process() +} diff --git a/common/tx_test.go b/common/tx_test.go new file mode 100644 index 0000000..4645024 --- /dev/null +++ b/common/tx_test.go @@ -0,0 +1,74 @@ +package common + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "testing" + "time" +) + +func TestNewTx(t *testing.T) { + senderPublicKey, senderPrivateKey, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatalf("failed to generate key pair: %s", err.Error()) + } + _, receiverPublicKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("failed to generate key pair: %s", err.Error()) + } + newTxOpts := MakeNewTxOpts( + senderPublicKey, senderPrivateKey, receiverPublicKey, + 0.01, "", false) + + newTxOptsJson, err := newTxOpts.ToJSON(EncodingRawUrl) + if err != nil { + t.Fatalf("failed to marshal JSON to tx: %s", err.Error()) + } + newTxOptsFromJSON, err := NewTxOptsFromJSON(newTxOptsJson) + if err != nil { + t.Fatalf("failed to unmarshal tx from JSON: %s", err.Error()) + } + if ok := newTxOptsFromJSON.ValidateBlockId(); !ok { + t.Fatalf("invalid block id") + } + if ok := newTxOptsFromJSON.ValidateHash(); !ok { + t.Fatalf("invalid hash. Got hash: %s", base64.RawURLEncoding.EncodeToString(newTxOptsFromJSON.Hash)) + } + if ok := newTxOptsFromJSON.ValidateSignature(); !ok { + t.Fatalf("invalid signature") + } +} + +func TestTx(t *testing.T) { + senderPublicKey, senderPrivateKey, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatalf("failed to generate key pair: %s", err.Error()) + } + _, receiverPublicKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("failed to generate key pair: %s", err.Error()) + } + newTxOpts := MakeNewTxOpts( + senderPublicKey, senderPrivateKey, receiverPublicKey, + 0.01, "", false) + tx, _ := newTxOpts.Process(false, time.Time{}) + + txJson, err := tx.ToJSON(EncodingRawUrl) + if err != nil { + t.Fatalf("failed to marshal JSON to tx: %s", err.Error()) + } + txFromJson, err := TxFromJSON(txJson) + if err != nil { + t.Fatalf("failed to unmarshal tx from JSON: %s", err.Error()) + } + if ok := txFromJson.ValidateBlockId(); !ok { + t.Fatalf("invalid block id") + } + if ok := txFromJson.ValidateHash(); !ok { + t.Fatalf("invalid hash. Got hash: %s", base64.RawURLEncoding.EncodeToString(txFromJson.Hash)) + } + if ok := txFromJson.ValidateSignature(); !ok { + t.Fatalf("invalid signature") + } +} diff --git a/common/version.go b/common/version.go new file mode 100644 index 0000000..18f3090 --- /dev/null +++ b/common/version.go @@ -0,0 +1,7 @@ +package common + +import "fmt" + +var VersionNumber = []int{0, 1, 0} +var VersionNumberStr = fmt.Sprintf("v%d.%d.%d", + VersionNumber[0], VersionNumber[1], VersionNumber[2]) diff --git a/server/core/block.go b/server/core/block.go new file mode 100644 index 0000000..d407418 --- /dev/null +++ b/server/core/block.go @@ -0,0 +1,52 @@ +package core + +import ( + "crypto/sha512" + "fmt" + "mic-wallet/common" + "sync" +) + +type Block struct { + ID int64 + IgnoreSignatures map[string]struct{} + TXs map[string]*common.NewTxOpts + Lock *sync.RWMutex + PrevHash []byte + Hash []byte +} + +func NewBlock(id int64, prevHash []byte) *Block { + return &Block{ + ID: id, + IgnoreSignatures: make(map[string]struct{}), + TXs: make(map[string]*common.NewTxOpts), + Lock: &sync.RWMutex{}, + PrevHash: prevHash, + } +} + +func (b *Block) AddTx(tx *common.NewTxOpts) error { + b.Lock.Lock() + hashStr := tx.HashString() + sigStr := tx.SignatureString() + if _, ok := b.IgnoreSignatures[sigStr]; ok { + return fmt.Errorf("duplicate") + } + b.TXs[hashStr] = tx + b.Lock.Unlock() + return nil +} + +func (b *Block) RemoveTx(hash string) {} + +func (b *Block) CalcHash() []byte { + hash := sha512.New() + b.Lock.Lock() + hash.Write(b.PrevHash) + for _, tx := range b.TXs { + hash.Write(tx.Hash) + } + b.Hash = hash.Sum(nil) + return b.Hash +} diff --git a/server/core/block_pipeline.go b/server/core/block_pipeline.go deleted file mode 100644 index 2f080cb..0000000 --- a/server/core/block_pipeline.go +++ /dev/null @@ -1,47 +0,0 @@ -package core - -import ( - "context" - "mic-wallet/server/repository/entities" - "sync" - "time" -) - -type BlockData struct { - Lock *sync.RWMutex - prevHash []byte - Transactions map[string]entities.NewTransactionOpts -} - -// BlocksPipeline does block rotation, writes transaction into the block or block queue -// write blocks when they are finished. -type BlocksPipeline struct { - cooldown time.Duration - writeToBlock uint8 - block1 *BlockData - block2 *BlockData - block3 *BlockData -} - -func NewBlockPipeline(cooldown time.Duration) *BlocksPipeline { - return &BlocksPipeline{} -} - -func (b *BlocksPipeline) loadPrevBlockHash(ctx context.Context) { - -} - -func (b *BlocksPipeline) rotate(ctx context.Context) error { - - return nil -} - -func (b *BlocksPipeline) NewTransaction(ctx context.Context, opts NewTransactionOpts) error { - - return nil -} - -func (b *BlocksPipeline) Run(ctx context.Context) error { - - return nil -} diff --git a/server/core/blocks_commiter.go b/server/core/blocks_commiter.go new file mode 100644 index 0000000..8734037 --- /dev/null +++ b/server/core/blocks_commiter.go @@ -0,0 +1,84 @@ +package core + +import ( + "context" + "github.com/matchsystems/werr" + "mic-wallet/server/repository" + "sync" + "time" +) + +type CommitBlockFunc func(ctx context.Context, block *Block) error + +type IBlocksCommiter interface { + // CommitBlock pushes blocks into the storage (db). + // Block must be not null + CommitBlock(ctx context.Context, block *Block) +} + +type BlocksCommiter struct { + BlocksQueue []*Block + Lock *sync.RWMutex + CountLim int32 + CountBusy int32 + TaskDeadline time.Duration + ErrChan chan error + + CommitBlockFunc CommitBlockFunc +} + +func NewBlocksCommiter(repo *repository.Repository, errChan chan error, workersCountLim int) *BlocksCommiter { + return &BlocksCommiter{ + BlocksQueue: make([]*Block, 0), + Lock: &sync.RWMutex{}, + CountLim: 0, + CountBusy: 0, + TaskDeadline: 0, + ErrChan: make(chan error, workersCountLim), + CommitBlockFunc: func(ctx context.Context, block *Block) error { + return CommitBlock(ctx, repo, block) + }, + } +} + +func (bc *BlocksCommiter) execute(ctx context.Context, block *Block) { + if bc.TaskDeadline > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, bc.TaskDeadline) + defer cancel() + } + errChan := make(chan error) + go func() { + if err := bc.CommitBlockFunc(ctx, block); err != nil { + errChan <- err + } + }() + select { + case <-ctx.Done(): + 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) + } +} + +func (bc *BlocksCommiter) CommitBlock(ctx context.Context, block *Block) { + bc.Lock.Lock() + if bc.CountBusy >= bc.CountLim { + bc.BlocksQueue = append(bc.BlocksQueue, block) + return + } + bc.CountBusy++ + bc.Lock.Unlock() + for { + bc.execute(ctx, block) + bc.Lock.Lock() + if len(bc.BlocksQueue) < 1 { + bc.CountBusy-- + bc.Lock.Unlock() + return + } + block = bc.BlocksQueue[0] + bc.BlocksQueue = bc.BlocksQueue[1:] + bc.Lock.Unlock() + } +} diff --git a/server/core/blocks_creator.go b/server/core/blocks_creator.go new file mode 100644 index 0000000..9e7fc51 --- /dev/null +++ b/server/core/blocks_creator.go @@ -0,0 +1,56 @@ +package core + +import ( + "context" + "errors" + "github.com/matchsystems/werr" + "mic-wallet/common" + "time" +) + +type IBlocksCreator interface { + Run(ctx context.Context) error + WriteTx(opts *common.NewTxOpts) error + // GetTx Searches for tx in every block + GetTx(block int64, hash string) *common.NewTxOpts + GetTxs() []*common.NewTxOpts +} + +type BlocksCreator struct { + blocks map[int64]*Block + commiter IBlocksCommiter + prevBlockHash []byte +} + +func NewBlocksCreator(commiter IBlocksCommiter) *BlocksCreator { + return &BlocksCreator{ + blocks: make(map[int64]*Block), + commiter: commiter, + } +} + +func (bc *BlocksCreator) Run(ctx context.Context) error { + var blockID int64 + 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) + } + bc.blocks[blockID] = NewBlock(blockID, bc.prevBlockHash) + time.Sleep(time.Until(time.Unix(blockID+100, 0))) + } +} + +func (bc *BlocksCreator) WriteTx(opts *common.NewTxOpts) error { + 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") + } else if currentBlockID > opts.BlockId { + return errors.New("block id is out of date") + } + return werr.Wrapf(bc.blocks[currentBlockID].AddTx(opts), + "error adding transaction to block %d", currentBlockID) +} diff --git a/server/core/commit_block.go b/server/core/commit_block.go new file mode 100644 index 0000000..81ff9b1 --- /dev/null +++ b/server/core/commit_block.go @@ -0,0 +1,10 @@ +package core + +import ( + "context" + "mic-wallet/server/repository" +) + +func CommitBlock(ctx context.Context, repo *repository.Repository, block *Block) error { + return nil +} diff --git a/server/core/entities.go b/server/core/entities.go deleted file mode 100644 index cc0cb76..0000000 --- a/server/core/entities.go +++ /dev/null @@ -1,15 +0,0 @@ -package core - -import "time" - -type NewTransactionOpts struct { - Hash []byte - BlockId int64 - SenderPublicKey []byte - ReceiverPublicKey []byte - IsReward bool - Amount float64 - Message string - Signature []byte - CreatedAt time.Time -} diff --git a/server/logic/validate.go b/server/logic/validate.go deleted file mode 100644 index 81da0c5..0000000 --- a/server/logic/validate.go +++ /dev/null @@ -1,31 +0,0 @@ -package logic - -import ( - "bytes" - "errors" - "github.com/matchsystems/werr" - "math" - "mic-wallet/common" - "mic-wallet/server/repository/entities" - "time" -) - -func ValidateNewTransaction(tx *entities.NewTransactionOpts, txHash []byte) error { - if bytes.Compare(tx.ReceiverPublicKey, tx.SenderPublicKey) == 0 { - return errors.New("cannot send to yourself") - } - valid, err := common.VerifyTransactionSignature(tx.SenderPublicKey, tx.Signature, txHash, tx.CreatedAt) - if err != nil { - return werr.Wrapf(err, "failed to verify signature") - } - if !valid { - return errors.New("invalid signature") - } - - now := time.Now().UTC() - // Sent data has to be in UTC timezone - if time.Duration(math.Abs(float64(tx.CreatedAt.Sub(now)))) > time.Minute*1 { - return errors.New("invalid transaction created_at time") - } - return nil -} diff --git a/server/migrations/0.up.sql b/server/migrations/0.up.sql index 31971fe..6938e94 100644 --- a/server/migrations/0.up.sql +++ b/server/migrations/0.up.sql @@ -3,24 +3,27 @@ SET TIME ZONE 'UTC'; CREATE TABLE blocks ( - id BIGSERIAL PRIMARY KEY, + -- UNIX Timestamp + -- But not just any timestamp, some timestamp + id BIGINT PRIMARY KEY, -- SHA512 sum of all hashes inside the block + prev_hash -- 32 bytes long - hash BYTEA UNIQUE, + hash BYTEA UNIQUE, -- NULL only on the first block - prev_hash BYTEA UNIQUE, - started_at TIMESTAMP NOT NULL, + prev_hash BYTEA UNIQUE, finished_at TIMESTAMP, ) CREATE TABLE IF NOT EXISTS transactions ( - -- SHA512 -- 32 bytes long - hash BYTEA PRIMARY KEY, - block_id BIGINT REFERENCES blocks(id) NOT NULL, + -- SHA512 -- 64 bytes long + hash BYTEA(64) PRIMARY KEY, + -- references block id + -- but may be added before the block itself + block_id BIGINT, - -- ED25519 32 bytes long - sender_public_key BYTEA NOT NULL, - receiver_public_key BYTEA NOT NULL, + -- ED25519 key -- 32 bytes long + sender_public_key BYTEA(32) NOT NULL, + receiver_public_key BYTEA(32) NOT NULL, -- Rewards doesn't decrease sender's balance -- but generate additional crypto inside the system. @@ -34,19 +37,18 @@ CREATE TABLE IF NOT EXISTS transactions ( -- When sign is decrypted with the sender's -- public key we should get the created_at -- time in unix format - signature BYTEA NOT NULL, + signature BYTEA(24) NOT NULL, -- Time provided by the client - created_at TIMESTAMP, + created_at BIGINT NOT NULL, + created_at_ns BIGINT NOT NULL, -- Time when transaction was added to the server - added_at TIMESTAMP SET DEFAULT CURRENT TIMESTAMP, - - -- Prevent block duplicates - UNIQUE(sendrt_public_key, signature) + added_at TIMESTAMP SET DEFAULT CURRENT TIMESTAMP, -- NOTE: Will be extended with smart contracts data ); + CREATE INDEX idx_blocks_hash ON blocks (hash); -CREATE INDEX idx_blocks_prev_hash ON prev_hash (prev_hash); +CREATE INDEX idx_blocks_prev_hash ON blocks (prev_hash); CREATE INDEX idx_transactions_block_id ON transactions (block_id); CREATE INDEX idx_transactions_sender_pk ON transactions (sender_public_key); CREATE INDEX idx_transactions_receiver_pk ON transactions (receiver_public_key); \ No newline at end of file diff --git a/server/repository/entities/entities.go b/server/repository/entities/entities.go index 8702ed7..cb9b73b 100644 --- a/server/repository/entities/entities.go +++ b/server/repository/entities/entities.go @@ -49,6 +49,7 @@ type ( Message string `db:"message"` Signature []byte `db:"signature"` CreatedAt time.Time `db:"created_at"` + CreatedAtNs int64 `db:"created_at_ns"` AddedAt time.Time `db:"added_at"` } @@ -63,5 +64,6 @@ type ( Message string `db:"message"` Signature []byte `db:"signature"` CreatedAt time.Time `db:"created_at"` + CreatedAtNs int64 `db:"created_at_ns"` } ) diff --git a/server/setup.go b/server/setup.go index bf9e2d5..96fbbdd 100644 --- a/server/setup.go +++ b/server/setup.go @@ -18,8 +18,8 @@ import ( ) func (s *Server) Setup(ctx context.Context) error { - // ============================= - // Set up the transactions queue + // ================================ + // Set up the transactions pipeline // =================== // Setting up database @@ -79,7 +79,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 on address %s", addr) + return werr.Wrapf(err, "failed to listen http_api_entities on address %s", addr) } } if s.Cfg.HttpsPort > 0 {