Update abci.go

This commit is contained in:
Gregory Bednov 2025-08-10 10:22:19 +03:00
commit accf5ba341

View file

@ -1,7 +1,10 @@
package blockchain
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
@ -14,10 +17,172 @@ type PromiseApp struct {
currentBatch *badger.Txn
}
type CommiterTxBody struct {
Type string `json:"type"`
ID string `json:"id"`
Name string `json:"name"`
CommiterPubKey string `json:"commiter_pubkey"`
}
type PromiseTxBody struct {
Type string `json:"type"`
ID string `json:"id"`
Description string `json:"description"`
Timestamp int64 `json:"timestamp,omitempty"` // ← чтобы понимать клиент
Title string `json:"title,omitempty"` // ← опционально, если когда-нибудь пригодится
Deadline string `json:"deadline,omitempty"`
}
type CommitmentTxBody struct {
Type string `json:"type"`
ID string `json:"id"`
PromiseID string `json:"promise_id"`
CommiterID string `json:"commiter_id"`
CommiterSig string `json:"commiter_sig,omitempty"`
}
type compoundTxRaw struct {
Body json.RawMessage `json:"body"`
Signature string `json:"signature"`
}
type compoundBody struct {
Promise *PromiseTxBody `json:"promise"`
Commitment *CommitmentTxBody `json:"commitment"`
}
type CompoundTx struct {
Body struct {
Promise *PromiseTxBody `json:"promise"`
Commitment *CommitmentTxBody `json:"commitment"`
} `json:"body"`
Signature string `json:"signature"`
}
func NewPromiseApp(db *badger.DB) *PromiseApp {
return &PromiseApp{db: db}
}
func verifyAndExtractBody(db *badger.DB, tx []byte) (map[string]interface{}, error) {
var outer struct {
Body CommiterTxBody `json:"body"`
Signature string `json:"signature"`
}
if err := json.Unmarshal(tx, &outer); err != nil {
return nil, errors.New("invalid JSON wrapper")
}
msg, err := json.Marshal(outer.Body)
if err != nil {
return nil, err
}
sig, err := base64.StdEncoding.DecodeString(outer.Signature)
if err != nil {
return nil, errors.New("invalid signature base64")
}
if len(sig) != ed25519.SignatureSize {
return nil, fmt.Errorf("invalid signature length: got %d, want %d", len(sig), ed25519.SignatureSize)
}
pubkeyB64 := strings.TrimSpace(outer.Body.CommiterPubKey)
if pubkeyB64 == "" {
return nil, errors.New("missing commiter pubkey")
}
pubkey, err := base64.StdEncoding.DecodeString(pubkeyB64)
if err != nil {
return nil, errors.New("invalid pubkey base64")
}
if len(pubkey) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid pubkey length: got %d, want %d", len(pubkey), ed25519.PublicKeySize)
}
if !ed25519.Verify(pubkey, msg, sig) {
return nil, errors.New("signature verification failed")
}
var result map[string]interface{}
if err := json.Unmarshal(msg, &result); err != nil {
return nil, err
}
return result, nil
}
func (app *PromiseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
compound, err := verifyCompoundTx(app.db, req.Tx)
if err == nil {
// Проверка на уникальность Promise.ID
if compound.Body.Promise != nil {
err := app.db.View(func(txn *badger.Txn) error {
_, err := txn.Get([]byte(compound.Body.Promise.ID))
if err == badger.ErrKeyNotFound {
return nil
}
return errors.New("duplicate promise ID")
})
if err != nil {
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
}
}
// Проверка на уникальность Commitment.ID
if compound.Body.Commitment != nil {
err := app.db.View(func(txn *badger.Txn) error {
_, err := txn.Get([]byte(compound.Body.Commitment.ID))
if err == badger.ErrKeyNotFound {
return nil
}
return errors.New("duplicate commitment ID")
})
if err != nil {
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
}
}
// Проверка на существование коммитера
if compound.Body.Commitment != nil {
err := app.db.View(func(txn *badger.Txn) error {
_, err := txn.Get([]byte(compound.Body.Commitment.CommiterID))
if err == badger.ErrKeyNotFound {
return errors.New("unknown commiter")
}
return nil
})
if err != nil {
return abci.ResponseCheckTx{Code: 4, Log: err.Error()}
}
}
return abci.ResponseCheckTx{Code: 0}
}
// Попытка старого формата только если он реально валиден;
// иначе возвращаем исходную причину из verifyCompoundTx.
if body, oldErr := verifyAndExtractBody(app.db, req.Tx); oldErr == nil {
id, ok := body["id"].(string)
if !ok || id == "" {
return abci.ResponseCheckTx{Code: 2, Log: "missing id"}
}
// Проверка на дубликат
err := app.db.View(func(txn *badger.Txn) error {
_, err := txn.Get([]byte(id))
if err == badger.ErrKeyNotFound {
return nil
}
return errors.New("duplicate id")
})
if err != nil {
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
}
return abci.ResponseCheckTx{Code: 0}
}
// Здесь: составной формат не прошёл — вернём ПРИЧИНУ.
return abci.ResponseCheckTx{Code: 1, Log: err.Error()}
}
func (app *PromiseApp) BeginBlock(_ abci.RequestBeginBlock) abci.ResponseBeginBlock {
if app.currentBatch != nil {
app.currentBatch.Discard()
@ -27,52 +192,145 @@ func (app *PromiseApp) BeginBlock(_ abci.RequestBeginBlock) abci.ResponseBeginBl
}
func (app *PromiseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx {
var tx map[string]interface{}
if err := json.Unmarshal(req.Tx, &tx); err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: "invalid JSON"}
}
compound, err := verifyCompoundTx(app.db, req.Tx)
if err != nil {
outer := struct {
Body CommiterTxBody `json:"body"`
Signature string `json:"signature"`
}{}
if err := json.Unmarshal(req.Tx, &outer); err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: "invalid tx format"}
}
// Валидация подписи/ключа одиночного коммитера перед записью
if _, vErr := verifyAndExtractBody(app.db, req.Tx); vErr != nil {
return abci.ResponseDeliverTx{Code: 1, Log: vErr.Error()}
}
id, ok := tx["id"].(string)
if !ok || id == "" {
return abci.ResponseDeliverTx{Code: 2, Log: "missing id"}
}
id := outer.Body.ID
if id == "" {
return abci.ResponseDeliverTx{Code: 1, Log: "missing id"}
}
data, _ := json.Marshal(tx)
if app.currentBatch == nil {
app.currentBatch = app.db.NewTransaction(true)
}
data, err := json.Marshal(outer.Body)
if err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: err.Error()}
}
if err = app.currentBatch.Set([]byte(id), data); err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: err.Error()}
}
return abci.ResponseDeliverTx{Code: 0}
}
if app.currentBatch == nil {
app.currentBatch = app.db.NewTransaction(true)
}
err := app.currentBatch.Set([]byte(id), data)
if err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: fmt.Sprintf("failed to set: %v", err)}
if compound.Body.Promise != nil {
data, _ := json.Marshal(compound.Body.Promise)
err := app.currentBatch.Set([]byte(compound.Body.Promise.ID), data)
if err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: "failed to save promise"}
}
}
if compound.Body.Commitment != nil {
data, _ := json.Marshal(compound.Body.Commitment)
err := app.currentBatch.Set([]byte(compound.Body.Commitment.ID), data)
if err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: "failed to save commitment"}
}
}
return abci.ResponseDeliverTx{Code: 0}
}
func (app *PromiseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
var tx map[string]interface{}
if err := json.Unmarshal(req.Tx, &tx); err != nil {
return abci.ResponseCheckTx{Code: 1, Log: "invalid JSON"}
func verifyCompoundTx(db *badger.DB, tx []byte) (*CompoundTx, error) {
// 1) Разобрать внешний конверт, body оставить сырым
var outerRaw compoundTxRaw
if err := json.Unmarshal(tx, &outerRaw); err != nil {
return nil, errors.New("invalid compound tx JSON")
}
id, ok := tx["id"].(string)
if !ok || id == "" {
return abci.ResponseCheckTx{Code: 2, Log: "missing id"}
if len(outerRaw.Body) == 0 {
return nil, errors.New("missing body")
}
// проверка уникальности id
err := app.db.View(func(txn *badger.Txn) error {
_, err := txn.Get([]byte(id))
if err == badger.ErrKeyNotFound {
return nil
// 2) Вынуть commiter_id из body, не парся всё
var tiny struct {
Commitment *struct {
CommiterID string `json:"commiter_id"`
} `json:"commitment"`
}
if err := json.Unmarshal(outerRaw.Body, &tiny); err != nil {
return nil, errors.New("invalid body JSON")
}
if tiny.Commitment == nil || strings.TrimSpace(tiny.Commitment.CommiterID) == "" {
return nil, errors.New("missing commitment")
}
commiterID := tiny.Commitment.CommiterID
// 3) Достать коммитера из БД и получить публичный ключ
var commiterData []byte
err := db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(commiterID))
if err != nil {
return errors.New("unknown commiter")
}
return fmt.Errorf("id exists")
return item.Value(func(v []byte) error {
commiterData = append([]byte{}, v...)
return nil
})
})
if err != nil {
return abci.ResponseCheckTx{Code: 3, Log: "duplicate id"}
return nil, err
}
var commiter CommiterTxBody
if err := json.Unmarshal(commiterData, &commiter); err != nil {
return nil, errors.New("corrupted commiter record")
}
pubkeyB64 := strings.TrimSpace(commiter.CommiterPubKey)
if pubkeyB64 == "" {
return nil, errors.New("missing commiter pubkey")
}
pubkey, err := base64.StdEncoding.DecodeString(pubkeyB64)
if err != nil {
return nil, errors.New("invalid pubkey base64")
}
if len(pubkey) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid pubkey length: got %d, want %d", len(pubkey), ed25519.PublicKeySize)
}
return abci.ResponseCheckTx{Code: 0}
// 4) Проверка подписи над СЫРЫМИ БАЙТАМИ body (как клиент подписывал)
sig, err := base64.StdEncoding.DecodeString(outerRaw.Signature)
if err != nil {
return nil, errors.New("invalid signature base64")
}
if len(sig) != ed25519.SignatureSize {
return nil, fmt.Errorf("invalid signature length: got %d, want %d", len(sig), ed25519.SignatureSize)
}
if !ed25519.Verify(pubkey, outerRaw.Body, sig) {
return nil, errors.New("signature verification failed")
}
// 5) Только после успешной проверки — распарсить body в наши структуры
var body compoundBody
if err := json.Unmarshal(outerRaw.Body, &body); err != nil {
return nil, errors.New("invalid body JSON")
}
// 6) Вернуть в привычной форме
return &CompoundTx{
Body: struct {
Promise *PromiseTxBody `json:"promise"`
Commitment *CommitmentTxBody `json:"commitment"`
}{
Promise: body.Promise,
Commitment: body.Commitment,
},
Signature: outerRaw.Signature,
}, nil
}
func (app *PromiseApp) Commit() abci.ResponseCommit {
@ -86,7 +344,6 @@ func (app *PromiseApp) Commit() abci.ResponseCommit {
return abci.ResponseCommit{Data: []byte{}}
}
// SELECT * FROM <table>
func (app *PromiseApp) Query(req abci.RequestQuery) abci.ResponseQuery {
parts := strings.Split(strings.Trim(req.Path, "/"), "/")
if len(parts) != 2 || parts[0] != "list" {
@ -98,7 +355,6 @@ func (app *PromiseApp) Query(req abci.RequestQuery) abci.ResponseQuery {
err := app.db.View(func(txn *badger.Txn) error {
it := txn.NewIterator(badger.DefaultIteratorOptions)
defer it.Close()
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
item := it.Item()
err := item.Value(func(v []byte) error {
@ -117,12 +373,10 @@ func (app *PromiseApp) Query(req abci.RequestQuery) abci.ResponseQuery {
if err != nil {
return abci.ResponseQuery{Code: 1, Log: err.Error()}
}
all, _ := json.Marshal(result)
return abci.ResponseQuery{Code: 0, Value: all}
}
// Остальные методы ABCI
func (app *PromiseApp) Info(req abci.RequestInfo) abci.ResponseInfo {
return abci.ResponseInfo{Data: "promises", Version: "0.1"}
}