lbc/blockchain/abci.go
Gregory Bednov dae1cd484e Add ER diagram and schema docs, enhance tx validation
Added ER.svg and database_schema.md to document the logical data model. Refactored and extended transaction validation in blockchain/abci.go to enforce ID prefix rules, entity existence, and uniqueness for promises, commitments, commiters, and beneficiaries. Updated lbc_sdk dependency version in go.mod and go.sum.
2025-08-10 22:39:28 +03:00

484 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package blockchain
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
types "github.com/gregorybednov/lbc_sdk"
"github.com/dgraph-io/badger"
abci "github.com/tendermint/tendermint/abci/types"
)
type PromiseApp struct {
db *badger.DB
currentBatch *badger.Txn
}
type compoundTxRaw struct {
Body json.RawMessage `json:"body"`
Signature string `json:"signature"`
}
type compoundBody struct {
Promise *types.PromiseTxBody `json:"promise"`
Commitment *types.CommitmentTxBody `json:"commitment"`
}
func NewPromiseApp(db *badger.DB) *PromiseApp {
return &PromiseApp{db: db}
}
func hasPrefix(id, pref string) bool { return strings.HasPrefix(id, pref+":") }
func requireIDPrefix(id, pref string) error {
if strings.TrimSpace(id) == "" {
return fmt.Errorf("missing %s id", pref)
}
if !hasPrefix(id, pref) {
return fmt.Errorf("invalid %s id prefix", pref)
}
return nil
}
func verifyAndExtractBody(db *badger.DB, tx []byte) (map[string]interface{}, error) {
var outer struct {
Body types.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 {
// ---- Валидация содержимого композита по ER ----
p := compound.Body.Promise
c := compound.Body.Commitment
// Оба тела должны присутствовать
if p == nil || c == nil {
return abci.ResponseCheckTx{Code: 1, Log: "compound must include promise and commitment"}
}
// ID-предикаты и префиксы
if err := requireIDPrefix(p.ID, "promise"); err != nil {
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
}
if err := requireIDPrefix(c.ID, "commitment"); err != nil {
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
}
if err := requireIDPrefix(c.CommiterID, "commiter"); err != nil {
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
}
if err := requireIDPrefix(p.BeneficiaryID, "beneficiary"); err != nil {
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
}
if p.ParentPromiseID != nil {
if err := requireIDPrefix(*p.ParentPromiseID, "promise"); err != nil {
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
}
if *p.ParentPromiseID == p.ID {
return abci.ResponseCheckTx{Code: 2, Log: "parent_promise_id must not equal promise id"}
}
}
// Базовые обязательные поля Promise
if strings.TrimSpace(p.Text) == "" {
return abci.ResponseCheckTx{Code: 2, Log: "promise.text is required"}
}
if p.Due == 0 {
return abci.ResponseCheckTx{Code: 2, Log: "promise.due is required"}
}
// Commitment due
if c.Due == 0 {
return abci.ResponseCheckTx{Code: 2, Log: "commitment.due is required"}
}
// Связность по ER
if c.PromiseID != p.ID {
return abci.ResponseCheckTx{Code: 2, Log: "commitment.promise_id must equal promise.id"}
}
// Уникальность Promise.ID
if err := app.db.View(func(txn *badger.Txn) error {
_, e := txn.Get([]byte(p.ID))
if e == badger.ErrKeyNotFound {
return nil
}
return errors.New("duplicate promise ID")
}); err != nil {
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
}
// Уникальность Commitment.ID
if err := app.db.View(func(txn *badger.Txn) error {
_, e := txn.Get([]byte(c.ID))
if e == badger.ErrKeyNotFound {
return nil
}
return errors.New("duplicate commitment ID")
}); err != nil {
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
}
// Существование коммитера (у тебя уже было — оставляю)
if err := app.db.View(func(txn *badger.Txn) error {
_, e := txn.Get([]byte(c.CommiterID))
if e == badger.ErrKeyNotFound {
return errors.New("unknown commiter")
}
return nil
}); err != nil {
return abci.ResponseCheckTx{Code: 4, Log: err.Error()}
}
// Существование бенефициара
if err := app.db.View(func(txn *badger.Txn) error {
_, e := txn.Get([]byte(p.BeneficiaryID))
if e == badger.ErrKeyNotFound {
return errors.New("unknown beneficiary")
}
return nil
}); err != nil {
return abci.ResponseCheckTx{Code: 5, Log: err.Error()}
}
// Существование parent (если задан)
if p.ParentPromiseID != nil {
if err := app.db.View(func(txn *badger.Txn) error {
_, e := txn.Get([]byte(*p.ParentPromiseID))
if e == badger.ErrKeyNotFound {
return errors.New("unknown parent promise")
}
return nil
}); err != nil {
return abci.ResponseCheckTx{Code: 6, Log: err.Error()}
}
}
return abci.ResponseCheckTx{Code: 0}
}
// ---- Попытка ОДИНОЧНЫХ транзакций ----
// 3.1) Совместимость: одиночный commiter (твоя старая логика)
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"}
}
if err := requireIDPrefix(id, "commiter"); err != nil {
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
}
// Дубликат
if err := app.db.View(func(txn *badger.Txn) error {
_, e := txn.Get([]byte(id))
if e == badger.ErrKeyNotFound {
return nil
}
return errors.New("duplicate id")
}); err != nil {
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
}
return abci.ResponseCheckTx{Code: 0}
}
var single struct {
Body types.BeneficiaryTxBody `json:"body"`
Signature string `json:"signature"`
}
if err2 := json.Unmarshal(req.Tx, &single); err2 == nil && single.Body.Type == "beneficiary" {
if err := requireIDPrefix(single.Body.ID, "beneficiary"); err != nil {
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
}
if strings.TrimSpace(single.Body.Name) == "" {
return abci.ResponseCheckTx{Code: 2, Log: "beneficiary.name is required"}
}
// уникальность
if err := app.db.View(func(txn *badger.Txn) error {
_, e := txn.Get([]byte(single.Body.ID))
if e == badger.ErrKeyNotFound {
return nil
}
return errors.New("duplicate beneficiary ID")
}); 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()
}
app.currentBatch = app.db.NewTransaction(true)
return abci.ResponseBeginBlock{}
}
func (app *PromiseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx {
// Попытка композита
if compound, err := verifyCompoundTx(app.db, req.Tx); err == nil {
if app.currentBatch == nil {
app.currentBatch = app.db.NewTransaction(true)
}
if compound.Body.Promise != nil {
data, _ := json.Marshal(compound.Body.Promise)
if err := app.currentBatch.Set([]byte(compound.Body.Promise.ID), data); err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: "failed to save promise"}
}
}
if compound.Body.Commitment != nil {
data, _ := json.Marshal(compound.Body.Commitment)
if err := app.currentBatch.Set([]byte(compound.Body.Commitment.ID), data); err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: "failed to save commitment"}
}
}
return abci.ResponseDeliverTx{Code: 0}
}
// Одиночный commiter (как раньше)
{
var outer struct {
Body types.CommiterTxBody `json:"body"`
Signature string `json:"signature"`
}
if err := json.Unmarshal(req.Tx, &outer); err == nil && outer.Body.Type == "commiter" {
// сигнатуру проверяем прежней функцией
if _, vErr := verifyAndExtractBody(app.db, req.Tx); vErr != nil {
return abci.ResponseDeliverTx{Code: 1, Log: vErr.Error()}
}
if app.currentBatch == nil {
app.currentBatch = app.db.NewTransaction(true)
}
data, _ := json.Marshal(outer.Body)
if err := app.currentBatch.Set([]byte(outer.Body.ID), data); err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: err.Error()}
}
return abci.ResponseDeliverTx{Code: 0}
}
}
// Одиночный beneficiary
{
var outer struct {
Body types.BeneficiaryTxBody `json:"body"`
Signature string `json:"signature"`
}
if err := json.Unmarshal(req.Tx, &outer); err == nil && outer.Body.Type == "beneficiary" {
// (пока без проверки подписи — можно добавить политику позже)
if app.currentBatch == nil {
app.currentBatch = app.db.NewTransaction(true)
}
data, _ := json.Marshal(outer.Body)
if err := app.currentBatch.Set([]byte(outer.Body.ID), data); err != nil {
return abci.ResponseDeliverTx{Code: 1, Log: err.Error()}
}
return abci.ResponseDeliverTx{Code: 0}
}
}
return abci.ResponseDeliverTx{Code: 1, Log: "invalid tx format"}
}
func verifyCompoundTx(db *badger.DB, tx []byte) (*types.CompoundTx, error) {
// 1) Разобрать внешний конверт, body оставить сырым
var outerRaw compoundTxRaw
if err := json.Unmarshal(tx, &outerRaw); err != nil {
return nil, errors.New("invalid compound tx JSON")
}
if len(outerRaw.Body) == 0 {
return nil, errors.New("missing body")
}
// 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 item.Value(func(v []byte) error {
commiterData = append([]byte{}, v...)
return nil
})
})
if err != nil {
return nil, err
}
var commiter types.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)
}
// 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 &types.CompoundTx{
Body: struct {
Promise *types.PromiseTxBody `json:"promise"`
Commitment *types.CommitmentTxBody `json:"commitment"`
}{
Promise: body.Promise,
Commitment: body.Commitment,
},
Signature: outerRaw.Signature,
}, nil
}
func (app *PromiseApp) Commit() abci.ResponseCommit {
if app.currentBatch != nil {
err := app.currentBatch.Commit()
if err != nil {
fmt.Printf("Commit error: %v\n", err)
}
app.currentBatch = nil
}
return abci.ResponseCommit{Data: []byte{}}
}
func (app *PromiseApp) Query(req abci.RequestQuery) abci.ResponseQuery {
parts := strings.Split(strings.Trim(req.Path, "/"), "/")
if len(parts) != 2 || parts[0] != "list" {
return abci.ResponseQuery{Code: 1, Log: "unsupported query"}
}
prefix := []byte(parts[1] + ":")
var result []json.RawMessage
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 {
var raw json.RawMessage = make([]byte, len(v))
copy(raw, v)
result = append(result, raw)
return nil
})
if err != nil {
return err
}
}
return nil
})
if err != nil {
return abci.ResponseQuery{Code: 1, Log: err.Error()}
}
all, _ := json.Marshal(result)
return abci.ResponseQuery{Code: 0, Value: all}
}
func (app *PromiseApp) Info(req abci.RequestInfo) abci.ResponseInfo {
return abci.ResponseInfo{Data: "promises", Version: "0.1"}
}
func (app *PromiseApp) SetOption(req abci.RequestSetOption) abci.ResponseSetOption {
return abci.ResponseSetOption{}
}
func (app *PromiseApp) InitChain(req abci.RequestInitChain) abci.ResponseInitChain {
return abci.ResponseInitChain{}
}
func (app *PromiseApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock {
return abci.ResponseEndBlock{}
}
func (app *PromiseApp) ListSnapshots(req abci.RequestListSnapshots) abci.ResponseListSnapshots {
return abci.ResponseListSnapshots{}
}
func (app *PromiseApp) OfferSnapshot(req abci.RequestOfferSnapshot) abci.ResponseOfferSnapshot {
return abci.ResponseOfferSnapshot{Result: abci.ResponseOfferSnapshot_REJECT}
}
func (app *PromiseApp) LoadSnapshotChunk(req abci.RequestLoadSnapshotChunk) abci.ResponseLoadSnapshotChunk {
return abci.ResponseLoadSnapshotChunk{}
}
func (app *PromiseApp) ApplySnapshotChunk(req abci.RequestApplySnapshotChunk) abci.ResponseApplySnapshotChunk {
return abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT}
}