From dae1cd484e5454123c29982588b6b643a93b827e Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Sun, 10 Aug 2025 22:39:28 +0300 Subject: [PATCH] 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. --- blockchain/abci.go | 275 ++++++++++++++++++++++++++++------------ docs/ER.svg | 1 + docs/database_schema.md | 43 +++++++ go.mod | 2 +- go.sum | 2 + 5 files changed, 240 insertions(+), 83 deletions(-) create mode 100644 docs/ER.svg create mode 100644 docs/database_schema.md diff --git a/blockchain/abci.go b/blockchain/abci.go index 54929b5..006cae3 100644 --- a/blockchain/abci.go +++ b/blockchain/abci.go @@ -33,6 +33,19 @@ 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"` @@ -83,73 +96,162 @@ func verifyAndExtractBody(db *badger.DB, tx []byte) (map[string]interface{}, err 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 { + // ---- Валидация содержимого композита по 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()} } - } - - // Проверка на уникальность 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 *p.ParentPromiseID == p.ID { + return abci.ResponseCheckTx{Code: 2, Log: "parent_promise_id must not equal promise id"} } } - // Проверка на существование коммитера - 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") + // Базовые обязательные поля 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 - }) - if err != nil { - return abci.ResponseCheckTx{Code: 4, Log: err.Error()} + }); err != nil { + return abci.ResponseCheckTx{Code: 6, Log: err.Error()} } } return abci.ResponseCheckTx{Code: 0} } - // Попытка старого формата только если он реально валиден; - // иначе возвращаем исходную причину из verifyCompoundTx. + // ---- Попытка ОДИНОЧНЫХ транзакций ---- + + // 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"} } - // Проверка на дубликат - err := app.db.View(func(txn *badger.Txn) error { - _, err := txn.Get([]byte(id)) - if err == badger.ErrKeyNotFound { + 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") - }) - if err != nil { + }); 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()} } @@ -162,59 +264,68 @@ func (app *PromiseApp) BeginBlock(_ abci.RequestBeginBlock) abci.ResponseBeginBl } func (app *PromiseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { - compound, err := verifyCompoundTx(app.db, req.Tx) - if err != nil { - outer := struct { - Body types.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 := outer.Body.ID - if id == "" { - return abci.ResponseDeliverTx{Code: 1, Log: "missing id"} - } - + // Попытка композита + if compound, err := verifyCompoundTx(app.db, req.Tx); err == nil { 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 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 err = app.currentBatch.Set([]byte(id), data); err != nil { - return abci.ResponseDeliverTx{Code: 1, Log: err.Error()} + 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} } - if app.currentBatch == nil { - app.currentBatch = app.db.NewTransaction(true) - } - - 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"} + // Одиночный commiter (как раньше) + { + var outer struct { + Body types.CommiterTxBody `json:"body"` + Signature string `json:"signature"` } - } - 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"} + 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} } } - 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) { diff --git a/docs/ER.svg b/docs/ER.svg new file mode 100644 index 0000000..5bec34d --- /dev/null +++ b/docs/ER.svg @@ -0,0 +1 @@ +PromiseID: uuidtext: textdue: datetimeBeneficiaryID: uuidParentPromiseID: uuidBeneficiaryID: uuidname: stringCommitmentID: uuidPromiseID: intCommiterID: intdue: datetimeCommiterID: intname: stringbelongs tomade byhasparent of \ No newline at end of file diff --git a/docs/database_schema.md b/docs/database_schema.md new file mode 100644 index 0000000..2d405d9 --- /dev/null +++ b/docs/database_schema.md @@ -0,0 +1,43 @@ +# Логическая модель данных + +![[ER.svg]] + +
+@startuml + +entity Promise { + * ID: uuid + -- + * text: text + * due: datetime + BeneficiaryID: uuid + ParentPromiseID: uuid +} + +entity Beneficiary { + * ID: uuid + -- + * name: string +} + +entity Commitment { + * ID: uuid + -- + PromiseID: int + CommiterID: int + due: datetime +} + +entity Commiter { + * ID: int + -- + * name: string +} + +Commitment }|--|| Promise : belongs to +Commitment }|--|| Commiter : made by +Promise }o--|| Beneficiary : has +Promise }--o Promise : parent of + +@enduml +
\ No newline at end of file diff --git a/go.mod b/go.mod index 1d41186..7b3f7b1 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( github.com/google/orderedcode v0.0.1 // indirect github.com/google/pprof v0.0.0-20241017200806-017d972448fc // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/gregorybednov/lbc_sdk v0.0.0-20250810102513-432a51e65f76 // indirect + github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hjson/hjson-go/v4 v4.4.0 // indirect diff --git a/go.sum b/go.sum index 8a8637f..fca1357 100644 --- a/go.sum +++ b/go.sum @@ -197,6 +197,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregorybednov/lbc_sdk v0.0.0-20250810102513-432a51e65f76 h1:e3A+1v+Mjt8nuJcVnHuhHZuh4052KRLCUBpH4g74rVs= github.com/gregorybednov/lbc_sdk v0.0.0-20250810102513-432a51e65f76/go.mod h1:DBE00+SaYBtD4qw+nOtSTLuF6h9Ia4TkuBMJB+6krik= +github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa h1:8EuqAmsS94ju83o4aEIV8e2fdocdAJ9xVerKRSI+nDA= +github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa/go.mod h1:DBE00+SaYBtD4qw+nOtSTLuF6h9Ia4TkuBMJB+6krik= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=