From ab5c96ecbd4985de36ea63087e30ed4a892523ea Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Tue, 5 Aug 2025 13:02:14 +0300 Subject: [PATCH 01/15] modified: blockchain/abci.go modified: blockchain/main.go --- blockchain/abci.go | 197 +++++++++++++++++++-------------------------- blockchain/main.go | 4 +- 2 files changed, 85 insertions(+), 116 deletions(-) diff --git a/blockchain/abci.go b/blockchain/abci.go index 4ad3558..1c4bb86 100644 --- a/blockchain/abci.go +++ b/blockchain/abci.go @@ -1,67 +1,24 @@ package blockchain import ( - "bytes" + "encoding/json" "fmt" + "strings" "github.com/dgraph-io/badger" abci "github.com/tendermint/tendermint/abci/types" ) -type KVStoreApplication struct { +type PromiseApp struct { db *badger.DB currentBatch *badger.Txn } -func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { - return &KVStoreApplication{db: db} +func NewPromiseApp(db *badger.DB) *PromiseApp { + return &PromiseApp{db: db} } -// Проверка транзакции (CheckTx) -func (app *KVStoreApplication) isValid(tx []byte) uint32 { - parts := bytes.Split(tx, []byte("=")) - if len(parts) != 2 { - return 1 // неверный формат - } - key, value := parts[0], parts[1] - - // Проверяем, существует ли уже такая запись - err := app.db.View(func(txn *badger.Txn) error { - item, err := txn.Get(key) - if err != nil { - if err == badger.ErrKeyNotFound { - return nil // ключ не найден, все ок - } - return err - } - - return item.Value(func(val []byte) error { - if bytes.Equal(val, value) { - return fmt.Errorf("duplicate key-value") - } - return nil - }) - }) - - if err != nil { - if err.Error() == "duplicate key-value" { - return 2 // дубликат найден - } - // любая другая ошибка - return 1 - } - - return 0 // все проверки пройдены -} - -func (app *KVStoreApplication) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { - code := app.isValid(req.Tx) - return abci.ResponseCheckTx{Code: code, GasWanted: 1} -} - -// Начало блока -func (app *KVStoreApplication) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock { - // If there's an existing batch for some reason, discard it first +func (app *PromiseApp) BeginBlock(_ abci.RequestBeginBlock) abci.ResponseBeginBlock { if app.currentBatch != nil { app.currentBatch.Discard() } @@ -69,112 +26,124 @@ func (app *KVStoreApplication) BeginBlock(req abci.RequestBeginBlock) abci.Respo return abci.ResponseBeginBlock{} } -// Применение транзакции -func (app *KVStoreApplication) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { - code := app.isValid(req.Tx) - if code != 0 { - return abci.ResponseDeliverTx{Code: code} +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"} } - parts := bytes.Split(req.Tx, []byte("=")) + id, ok := tx["id"].(string) + if !ok || id == "" { + return abci.ResponseDeliverTx{Code: 2, Log: "missing id"} + } + + data, _ := json.Marshal(tx) + if app.currentBatch == nil { - // In case BeginBlock wasn't called or batch was discarded app.currentBatch = app.db.NewTransaction(true) } - - err := app.currentBatch.Set(parts[0], parts[1]) + err := app.currentBatch.Set([]byte(id), data) if err != nil { - return abci.ResponseDeliverTx{ - Code: 1, - Log: fmt.Sprintf("Failed to set key: %v", err), - } + return abci.ResponseDeliverTx{Code: 1, Log: fmt.Sprintf("failed to set: %v", err)} } return abci.ResponseDeliverTx{Code: 0} } -// Завершение блока и фиксация -func (app *KVStoreApplication) Commit() abci.ResponseCommit { +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"} + } + id, ok := tx["id"].(string) + if !ok || id == "" { + return abci.ResponseCheckTx{Code: 2, Log: "missing id"} + } + + // проверка уникальности id + err := app.db.View(func(txn *badger.Txn) error { + _, err := txn.Get([]byte(id)) + if err == badger.ErrKeyNotFound { + return nil + } + return fmt.Errorf("id exists") + }) + if err != nil { + return abci.ResponseCheckTx{Code: 3, Log: "duplicate id"} + } + + return abci.ResponseCheckTx{Code: 0} +} + +func (app *PromiseApp) Commit() abci.ResponseCommit { if app.currentBatch != nil { err := app.currentBatch.Commit() if err != nil { - // Log error but continue - in a real application, you might want - // to handle this more gracefully - fmt.Printf("Error committing batch: %v\n", err) + fmt.Printf("Commit error: %v\n", err) } app.currentBatch = nil } return abci.ResponseCommit{Data: []byte{}} } -// Обслуживание запросов Query -func (app *KVStoreApplication) Query(req abci.RequestQuery) abci.ResponseQuery { - resp := abci.ResponseQuery{Code: 0} +// SELECT * FROM +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 { - item, err := txn.Get(req.Data) - if err != nil { - return err - } + it := txn.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() - return item.Value(func(val []byte) error { - resp.Value = val - return nil - }) + 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 { - resp.Code = 1 - resp.Log = err.Error() + return abci.ResponseQuery{Code: 1, Log: err.Error()} } - return resp + all, _ := json.Marshal(result) + return abci.ResponseQuery{Code: 0, Value: all} } -// Добавляем недостающие методы ABCI интерфейса - -// Info возвращает информацию о приложении -func (app *KVStoreApplication) Info(req abci.RequestInfo) abci.ResponseInfo { - return abci.ResponseInfo{ - Data: "kvstore", - Version: "1.0.0", - AppVersion: 1, - LastBlockHeight: 0, - LastBlockAppHash: []byte{}, - } +// Остальные методы ABCI +func (app *PromiseApp) Info(req abci.RequestInfo) abci.ResponseInfo { + return abci.ResponseInfo{Data: "promises", Version: "0.1"} } - -// SetOption устанавливает опцию приложения -func (app *KVStoreApplication) SetOption(req abci.RequestSetOption) abci.ResponseSetOption { - return abci.ResponseSetOption{Code: 0} +func (app *PromiseApp) SetOption(req abci.RequestSetOption) abci.ResponseSetOption { + return abci.ResponseSetOption{} } - -// InitChain инициализирует блокчейн -func (app *KVStoreApplication) InitChain(req abci.RequestInitChain) abci.ResponseInitChain { +func (app *PromiseApp) InitChain(req abci.RequestInitChain) abci.ResponseInitChain { return abci.ResponseInitChain{} } - -// EndBlock сигнализирует о конце блока -func (app *KVStoreApplication) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { +func (app *PromiseApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { return abci.ResponseEndBlock{} } - -// ListSnapshots возвращает список доступных снапшотов -func (app *KVStoreApplication) ListSnapshots(req abci.RequestListSnapshots) abci.ResponseListSnapshots { +func (app *PromiseApp) ListSnapshots(req abci.RequestListSnapshots) abci.ResponseListSnapshots { return abci.ResponseListSnapshots{} } - -// OfferSnapshot предлагает снапшот приложению -func (app *KVStoreApplication) OfferSnapshot(req abci.RequestOfferSnapshot) abci.ResponseOfferSnapshot { +func (app *PromiseApp) OfferSnapshot(req abci.RequestOfferSnapshot) abci.ResponseOfferSnapshot { return abci.ResponseOfferSnapshot{Result: abci.ResponseOfferSnapshot_REJECT} } - -// LoadSnapshotChunk загружает часть снапшота -func (app *KVStoreApplication) LoadSnapshotChunk(req abci.RequestLoadSnapshotChunk) abci.ResponseLoadSnapshotChunk { +func (app *PromiseApp) LoadSnapshotChunk(req abci.RequestLoadSnapshotChunk) abci.ResponseLoadSnapshotChunk { return abci.ResponseLoadSnapshotChunk{} } - -// ApplySnapshotChunk применяет часть снапшота -func (app *KVStoreApplication) ApplySnapshotChunk(req abci.RequestApplySnapshotChunk) abci.ResponseApplySnapshotChunk { +func (app *PromiseApp) ApplySnapshotChunk(req abci.RequestApplySnapshotChunk) abci.ResponseApplySnapshotChunk { return abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT} } diff --git a/blockchain/main.go b/blockchain/main.go index 092d159..fec3583 100644 --- a/blockchain/main.go +++ b/blockchain/main.go @@ -63,7 +63,7 @@ func GetNodeInfo(config *cfg.Config, dbPath string) (p2p.NodeInfo, error) { } defer db.Close() - app := NewKVStoreApplication(db) + app := NewPromiseApp(db) nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) if err != nil { @@ -110,7 +110,7 @@ func Run(ctx context.Context, dbPath string, config *cfg.Config, laddrReturner c } defer db.Close() - app := NewKVStoreApplication(db) + app := NewPromiseApp(db) node, err := newTendermint(app, config, laddrReturner) if err != nil { return fmt.Errorf("build node: %w", err) From accf5ba341a3efe4ffff7ccd3a6d389e0d1e4335 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Sun, 10 Aug 2025 10:22:19 +0300 Subject: [PATCH 02/15] Update abci.go --- blockchain/abci.go | 316 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 285 insertions(+), 31 deletions(-) diff --git a/blockchain/abci.go b/blockchain/abci.go index 1c4bb86..8da06af 100644 --- a/blockchain/abci.go +++ b/blockchain/abci.go @@ -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
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"} } From 0b102f2272206ed682b586a2d40d41d486e39719 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Sun, 10 Aug 2025 12:17:27 +0300 Subject: [PATCH 03/15] types extracted to submodule --- blockchain/abci.go | 55 +++++++++------------------------------ blockchain/types/types.go | 36 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 43 deletions(-) create mode 100644 blockchain/types/types.go diff --git a/blockchain/abci.go b/blockchain/abci.go index 8da06af..fefce2b 100644 --- a/blockchain/abci.go +++ b/blockchain/abci.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "lbc/blockchain/types" "strings" "github.com/dgraph-io/badger" @@ -17,46 +18,14 @@ 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"` + Promise *types.PromiseTxBody `json:"promise"` + Commitment *types.CommitmentTxBody `json:"commitment"` } func NewPromiseApp(db *badger.DB) *PromiseApp { @@ -65,8 +34,8 @@ func NewPromiseApp(db *badger.DB) *PromiseApp { func verifyAndExtractBody(db *badger.DB, tx []byte) (map[string]interface{}, error) { var outer struct { - Body CommiterTxBody `json:"body"` - Signature string `json:"signature"` + Body types.CommiterTxBody `json:"body"` + Signature string `json:"signature"` } if err := json.Unmarshal(tx, &outer); err != nil { @@ -195,8 +164,8 @@ func (app *PromiseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliver compound, err := verifyCompoundTx(app.db, req.Tx) if err != nil { outer := struct { - Body CommiterTxBody `json:"body"` - Signature string `json:"signature"` + 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"} @@ -247,7 +216,7 @@ func (app *PromiseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliver return abci.ResponseDeliverTx{Code: 0} } -func verifyCompoundTx(db *badger.DB, tx []byte) (*CompoundTx, error) { +func verifyCompoundTx(db *badger.DB, tx []byte) (*types.CompoundTx, error) { // 1) Разобрать внешний конверт, body оставить сырым var outerRaw compoundTxRaw if err := json.Unmarshal(tx, &outerRaw); err != nil { @@ -286,7 +255,7 @@ func verifyCompoundTx(db *badger.DB, tx []byte) (*CompoundTx, error) { if err != nil { return nil, err } - var commiter CommiterTxBody + var commiter types.CommiterTxBody if err := json.Unmarshal(commiterData, &commiter); err != nil { return nil, errors.New("corrupted commiter record") } @@ -321,10 +290,10 @@ func verifyCompoundTx(db *badger.DB, tx []byte) (*CompoundTx, error) { } // 6) Вернуть в привычной форме - return &CompoundTx{ + return &types.CompoundTx{ Body: struct { - Promise *PromiseTxBody `json:"promise"` - Commitment *CommitmentTxBody `json:"commitment"` + Promise *types.PromiseTxBody `json:"promise"` + Commitment *types.CommitmentTxBody `json:"commitment"` }{ Promise: body.Promise, Commitment: body.Commitment, diff --git a/blockchain/types/types.go b/blockchain/types/types.go new file mode 100644 index 0000000..6c280ca --- /dev/null +++ b/blockchain/types/types.go @@ -0,0 +1,36 @@ +package types + +// Types subpackage. +// You can you use to make an RFC client for abci application. + +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 CompoundTx struct { + Body struct { + Promise *PromiseTxBody `json:"promise"` + Commitment *CommitmentTxBody `json:"commitment"` + } `json:"body"` + Signature string `json:"signature"` +} From cf4580a8de61b16a1666cdb48123d597ab029b10 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Sun, 10 Aug 2025 12:22:09 +0300 Subject: [PATCH 04/15] changed module name with github official url --- blockchain/abci.go | 3 ++- blockchain/main.go | 3 ++- cfg/configfunctions.go | 3 ++- cli/init.go | 5 +++-- cli/root.go | 7 ++++--- cli/testyggdrasil.go | 5 +++-- go.mod | 2 +- main.go | 2 +- 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/blockchain/abci.go b/blockchain/abci.go index fefce2b..1e0830e 100644 --- a/blockchain/abci.go +++ b/blockchain/abci.go @@ -6,9 +6,10 @@ import ( "encoding/json" "errors" "fmt" - "lbc/blockchain/types" "strings" + "github.com/gregorybednov/lbc/blockchain/types" + "github.com/dgraph-io/badger" abci "github.com/tendermint/tendermint/abci/types" ) diff --git a/blockchain/main.go b/blockchain/main.go index fec3583..4baa991 100644 --- a/blockchain/main.go +++ b/blockchain/main.go @@ -4,9 +4,10 @@ import ( "context" "fmt" - "lbc/cfg" "os" + "github.com/gregorybednov/lbc/cfg" + "github.com/dgraph-io/badger" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" diff --git a/cfg/configfunctions.go b/cfg/configfunctions.go index b4705d9..a8d9b26 100644 --- a/cfg/configfunctions.go +++ b/cfg/configfunctions.go @@ -6,12 +6,13 @@ import ( "flag" "fmt" "io" - "lbc/yggdrasil" "os" "path/filepath" "strconv" "time" + "github.com/gregorybednov/lbc/yggdrasil" + "github.com/spf13/viper" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/p2p" diff --git a/cli/init.go b/cli/init.go index 77fab6d..2bdab7b 100644 --- a/cli/init.go +++ b/cli/init.go @@ -2,11 +2,12 @@ package cli import ( "fmt" - "lbc/blockchain" - "lbc/cfg" "os" "path/filepath" + "github.com/gregorybednov/lbc/blockchain" + "github.com/gregorybednov/lbc/cfg" + "github.com/spf13/cobra" ) diff --git a/cli/root.go b/cli/root.go index e582de6..7375875 100644 --- a/cli/root.go +++ b/cli/root.go @@ -3,13 +3,14 @@ package cli import ( "context" "fmt" - "lbc/blockchain" - "lbc/cfg" - "lbc/yggdrasil" "os" "os/signal" "syscall" + "github.com/gregorybednov/lbc/blockchain" + "github.com/gregorybednov/lbc/cfg" + "github.com/gregorybednov/lbc/yggdrasil" + "github.com/spf13/cobra" ) diff --git a/cli/testyggdrasil.go b/cli/testyggdrasil.go index 58f8b01..df3405e 100644 --- a/cli/testyggdrasil.go +++ b/cli/testyggdrasil.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "io" - "lbc/cfg" - "lbc/yggdrasil" "log" "net" "net/url" @@ -15,6 +13,9 @@ import ( "syscall" "time" + "github.com/gregorybednov/lbc/cfg" + "github.com/gregorybednov/lbc/yggdrasil" + "github.com/spf13/cobra" ) diff --git a/go.mod b/go.mod index e9944ee..ecb89e7 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module lbc +module github.com/gregorybednov/lbc go 1.24.3 diff --git a/main.go b/main.go index 2e6c6fb..5aeb0d8 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "lbc/cli" + "github.com/gregorybednov/lbc/cli" ) func main() { From a63abd641af6597b3cfaf911891a4c90ae47df86 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Sun, 10 Aug 2025 12:40:53 +0300 Subject: [PATCH 05/15] Update types.go --- blockchain/types/types.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/blockchain/types/types.go b/blockchain/types/types.go index 6c280ca..96243d4 100644 --- a/blockchain/types/types.go +++ b/blockchain/types/types.go @@ -34,3 +34,8 @@ type CompoundTx struct { } `json:"body"` Signature string `json:"signature"` } + +type SignedTx struct { + Body any `json:"body"` + Signature string `json:"signature"` +} From 57c40d9f073c8443a297f3e975aeb7377500ad65 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Sun, 10 Aug 2025 13:37:06 +0300 Subject: [PATCH 06/15] types is now in lbc_sdk repo --- blockchain/abci.go | 2 +- blockchain/types/types.go | 41 --------------------------------------- go.mod | 1 + go.sum | 2 ++ 4 files changed, 4 insertions(+), 42 deletions(-) delete mode 100644 blockchain/types/types.go diff --git a/blockchain/abci.go b/blockchain/abci.go index 1e0830e..54929b5 100644 --- a/blockchain/abci.go +++ b/blockchain/abci.go @@ -8,7 +8,7 @@ import ( "fmt" "strings" - "github.com/gregorybednov/lbc/blockchain/types" + types "github.com/gregorybednov/lbc_sdk" "github.com/dgraph-io/badger" abci "github.com/tendermint/tendermint/abci/types" diff --git a/blockchain/types/types.go b/blockchain/types/types.go deleted file mode 100644 index 96243d4..0000000 --- a/blockchain/types/types.go +++ /dev/null @@ -1,41 +0,0 @@ -package types - -// Types subpackage. -// You can you use to make an RFC client for abci application. - -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 CompoundTx struct { - Body struct { - Promise *PromiseTxBody `json:"promise"` - Commitment *CommitmentTxBody `json:"commitment"` - } `json:"body"` - Signature string `json:"signature"` -} - -type SignedTx struct { - Body any `json:"body"` - Signature string `json:"signature"` -} diff --git a/go.mod b/go.mod index ecb89e7..1d41186 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +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/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 fbd0cf5..8a8637f 100644 --- a/go.sum +++ b/go.sum @@ -195,6 +195,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 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/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= From dae1cd484e5454123c29982588b6b643a93b827e Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Sun, 10 Aug 2025 22:39:28 +0300 Subject: [PATCH 07/15] 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= From bf5371ea8b3b689dce2198041cf5ce82e820ac3e Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Tue, 12 Aug 2025 13:23:24 +0300 Subject: [PATCH 08/15] dynamic P2P external_address added --- blockchain/main.go | 1 + yggdrasil/main.go | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/blockchain/main.go b/blockchain/main.go index 4baa991..e80d8a4 100644 --- a/blockchain/main.go +++ b/blockchain/main.go @@ -24,6 +24,7 @@ func openBadger(path string) (*badger.DB, error) { func newTendermint(app abci.Application, config *cfg.Config, laddrReturner chan string) (*nm.Node, error) { config.P2P.ListenAddress = "tcp://" + <-laddrReturner + config.P2P.ExternalAddress = <-laddrReturner config.P2P.PersistentPeers = <-laddrReturner var pv tmTypes.PrivValidator diff --git a/yggdrasil/main.go b/yggdrasil/main.go index 6d81bbb..4e86711 100644 --- a/yggdrasil/main.go +++ b/yggdrasil/main.go @@ -62,7 +62,7 @@ func Yggdrasil(config *viper.Viper, ch chan string) { parsed, err := ParseEntries(peers) if err != nil { parsed = []ParsedEntry{} - ch <- "" + // ch <- "" log.Warnln("Warning: persistent peers has an error") } @@ -141,11 +141,18 @@ func Yggdrasil(config *viper.Viper, ch chan string) { panic(err) } address, subnet := n.core.Address(), n.core.Subnet() - publicstr := hex.EncodeToString(n.core.PublicKey()) - logger.Printf("Your public key is %s", publicstr) + yggPort := 26656 + if len(remoteTcp) > 0 && remoteTcp[0].Listen.Port != 0 { + yggPort = remoteTcp[0].Listen.Port + } + ipStr := address.String() // ожидается чистый IPv6 без /префикса + yggExternal := fmt.Sprintf("[%s]:%d", ipStr, yggPort) + ch <- yggExternal + + //logger.Printf("Your public key is %s", publicstr) logger.Printf("Your IPv6 address is %s", address.String()) logger.Printf("Your IPv6 subnet is %s", subnet.String()) - logger.Printf("Your Yggstack resolver name is %s%s", publicstr, types.NameMappingSuffix) + //logger.Printf("Your Yggstack resolver name is %s%s", publicstr, types.NameMappingSuffix) } // Setup the admin socket. From fb3e47117227187f88af5a73e6bd13796f8bba0d Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Tue, 12 Aug 2025 14:54:35 +0300 Subject: [PATCH 09/15] changed depth of channel --- cli/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/root.go b/cli/root.go index 7375875..1a7efc4 100644 --- a/cli/root.go +++ b/cli/root.go @@ -51,7 +51,7 @@ var rootCmd = &cobra.Command{ } ctx, cancel := context.WithCancel(context.Background()) - laddrReturner := make(chan string, 2) + laddrReturner := make(chan string, 3) go yggdrasil.Yggdrasil(v, laddrReturner) go blockchain.Run(ctx, dbPath, config, laddrReturner) From cdbef17af3319fe247cb2a37a38f79bfd193ef8a Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Tue, 12 Aug 2025 16:54:32 +0300 Subject: [PATCH 10/15] duplicate IPs allowed --- cfg/configfunctions.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/cfg/configfunctions.go b/cfg/configfunctions.go index a8d9b26..6980a55 100644 --- a/cfg/configfunctions.go +++ b/cfg/configfunctions.go @@ -164,16 +164,17 @@ func WriteConfig(config *cfg.Config, configPath *string, nodeInfo p2p.NodeInfo) config.P2P.PersistentPeers = a } - v.Set("p2p", map[string]interface{}{ - "use_legacy": false, - "queue_type": "priority", - "laddr": strconv.Itoa(yggListenPort) + ":127.0.0.1:8000", - "external_address": "", // will be set automatically by Tendermint if needed - "upnp": false, - "bootstrap_peers": "", - "persistent_peers": config.P2P.PersistentPeers, - "addr_book_file": "config/addrbook.json", - "addr_book_strict": false, + v.Set("p2p", map[string]any{ + "use_legacy": false, + "queue_type": "priority", + "laddr": strconv.Itoa(yggListenPort) + ":127.0.0.1:8000", + "external_address": "", + "upnp": false, + "bootstrap_peers": "", + "persistent_peers": config.P2P.PersistentPeers, + "allow_duplicate_ip": true, // needed because of Yggdrasil proxy + "addr_book_file": "config/addrbook.json", + "addr_book_strict": false, }) err = v.WriteConfigAs(*configPath) From 388db03fc02fe48cfa029bc6ac3c1ba65efe0ddf Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Tue, 12 Aug 2025 23:40:20 +0300 Subject: [PATCH 11/15] suspending most annoying DUE checks maybe, not forever... --- blockchain/abci.go | 19 ++++++------ docs/database_schema.md | 64 ++++++++++++++++++++--------------------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/blockchain/abci.go b/blockchain/abci.go index 006cae3..d385501 100644 --- a/blockchain/abci.go +++ b/blockchain/abci.go @@ -45,7 +45,6 @@ func requireIDPrefix(id, pref string) error { return nil } - func verifyAndExtractBody(db *badger.DB, tx []byte) (map[string]interface{}, error) { var outer struct { Body types.CommiterTxBody `json:"body"` @@ -131,13 +130,13 @@ func (app *PromiseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { 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"} - } + //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"} - } + //if c.Due == 0 { + // return abci.ResponseCheckTx{Code: 2, Log: "commitment.due is required"} + //} // Связность по ER if c.PromiseID != p.ID { @@ -165,7 +164,7 @@ func (app *PromiseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { 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 { @@ -229,7 +228,7 @@ func (app *PromiseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { var single struct { Body types.BeneficiaryTxBody `json:"body"` - Signature string `json:"signature"` + 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 { @@ -310,7 +309,7 @@ func (app *PromiseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliver { var outer struct { Body types.BeneficiaryTxBody `json:"body"` - Signature string `json:"signature"` + Signature string `json:"signature"` } if err := json.Unmarshal(req.Tx, &outer); err == nil && outer.Body.Type == "beneficiary" { // (пока без проверки подписи — можно добавить политику позже) diff --git a/docs/database_schema.md b/docs/database_schema.md index 2d405d9..00ba10c 100644 --- a/docs/database_schema.md +++ b/docs/database_schema.md @@ -3,41 +3,41 @@ ![[ER.svg]]
-@startuml + @startuml -entity Promise { - * ID: uuid - -- - * text: text - * due: datetime - BeneficiaryID: uuid - ParentPromiseID: uuid -} + entity Promise { + * ID: uuid + -- + * text: string + due: datetime + BeneficiaryID: uuid + ParentPromiseID: uuid + } -entity Beneficiary { - * ID: uuid - -- - * name: string -} + entity Beneficiary { + * ID: uuid + -- + * name: string + } -entity Commitment { - * ID: uuid - -- - PromiseID: int - CommiterID: int - due: datetime -} + entity Commitment { + * ID: uuid + -- + PromiseID: uuid + CommiterID: uuid + due: datetime + } -entity Commiter { - * ID: int - -- - * name: string -} + entity Commiter { + * ID: uuid + -- + * name: string + } -Commitment }|--|| Promise : belongs to -Commitment }|--|| Commiter : made by -Promise }o--|| Beneficiary : has -Promise }--o Promise : parent of + Commitment }|--|| Promise : belongs to + Commitment }|--|| Commiter : made by + Promise }o--|| Beneficiary : has + Promise }--o Promise : parent of -@enduml -
\ No newline at end of file + @enduml + From ce4ea5c09ca28531f24613abbb669de44c1fe946 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Wed, 13 Aug 2025 00:46:33 +0300 Subject: [PATCH 12/15] Update abci.go --- blockchain/abci.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blockchain/abci.go b/blockchain/abci.go index d385501..e90f342 100644 --- a/blockchain/abci.go +++ b/blockchain/abci.go @@ -114,9 +114,9 @@ func (app *PromiseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { 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 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()} From e7a9e1092be5c652127f99018ddac64d8b79b600 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Mon, 1 Sep 2025 11:04:46 +0300 Subject: [PATCH 13/15] Update root.go --- cli/root.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/cli/root.go b/cli/root.go index 1a7efc4..007e214 100644 --- a/cli/root.go +++ b/cli/root.go @@ -3,8 +3,10 @@ package cli import ( "context" "fmt" + "io" "os" "os/signal" + "path/filepath" "syscall" "github.com/gregorybednov/lbc/blockchain" @@ -71,3 +73,35 @@ func Execute() { os.Exit(1) } } + +func ExecuteWithArgs(args []string, home string, stdout, stderr io.Writer) error { + // Перенаправляем вывод Cobra + if stdout != nil { + rootCmd.SetOut(stdout) + } + if stderr != nil { + rootCmd.SetErr(stderr) + } + + // Если задан home, подставим дефолты, на которые завязаны флаги + // (флаги привязаны к переменным через StringVar, поэтому смена переменных до Execute — норм.) + origCfg := defaultConfigPath + origDB := dbPath + + if home != "" { + defaultConfigPath = filepath.Join(home, "config", "config.toml") + // Примем convention: BADGER в (home)/data/badger, но если у тебя другое — поменяй строку ниже. + dbPath = filepath.Join(home, "data", "badger") + } + + // Важное: подаём именно те аргументы, которые хотел вызвать вызывающий код. + rootCmd.SetArgs(args) + + // Выполняем и аккуратно восстанавливаем глобальные дефолты. + err := rootCmd.Execute() + + defaultConfigPath = origCfg + dbPath = origDB + + return err +} From f65d0d1ee10cbaf7f96c9508024c2ebf3c485cb8 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Wed, 17 Sep 2025 21:57:46 +0300 Subject: [PATCH 14/15] create new dir fix --- cfg/configfunctions.go | 19 ++++++++++++------- cli/init.go | 10 +++++++--- cli/root.go | 4 ++-- cli/testyggdrasil.go | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/cfg/configfunctions.go b/cfg/configfunctions.go index 6980a55..b21d3b8 100644 --- a/cfg/configfunctions.go +++ b/cfg/configfunctions.go @@ -252,18 +252,23 @@ func UpdateGenesisJson(nodeInfo p2p.NodeInfo, v *viper.Viper, defaultConfigDirec } } -func InitGenesis(chainName, defaultConfigPath string) (*cfg.Config, *viper.Viper) { +func InitGenesis(chainName, defaultConfigPath string) (*cfg.Config, *viper.Viper, error) { config := cfg.DefaultConfig() config.RootDir = filepath.Dir(filepath.Dir(defaultConfigPath)) - nodeinfo := p2p.DefaultNodeInfo{} - viper := WriteConfig(config, &defaultConfigPath, nodeinfo) - if err := InitTendermintFiles(config, true, chainName); err != nil { - fmt.Fprintf(os.Stderr, "Failed to init files: %v\n", err) - panic(err) + // Создаём директорию, если её нет + if err := os.MkdirAll(config.RootDir, 0o755); err != nil { + return nil, nil, fmt.Errorf("failed to create config directory %s: %w", config.RootDir, err) } - return config, viper + nodeinfo := p2p.DefaultNodeInfo{} + viper := WriteConfig(config, &defaultConfigPath, nodeinfo) + + if err := InitTendermintFiles(config, true, chainName); err != nil { + return nil, nil, fmt.Errorf("failed to init tendermint files: %w", err) + } + + return config, viper, nil } func InitJoiner(chainName, defaultConfigPath, path string) error { diff --git a/cli/init.go b/cli/init.go index 2bdab7b..03c6747 100644 --- a/cli/init.go +++ b/cli/init.go @@ -18,7 +18,11 @@ var initCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { switch args[0] { case "genesis": - config, viper := cfg.InitGenesis(chainName, defaultConfigPath) + config, viper, err := cfg.InitGenesis(chainName, defaultConfigPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) + panic(err) + } nodeinfo, err := blockchain.GetNodeInfo(config, dbPath) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v", err) @@ -30,14 +34,14 @@ var initCmd = &cobra.Command{ case "join": if len(args) < 2 { fmt.Fprintln(os.Stderr, "Укажите путь к genesis.json") - os.Exit(1) + return } cfg.InitJoiner(chainName, defaultConfigPath, args[1]) fmt.Println("Joiner node initialized.") default: fmt.Fprintf(os.Stderr, "Неизвестный режим init: %s\n", args[0]) - os.Exit(1) + return } }, } diff --git a/cli/root.go b/cli/root.go index 007e214..303956c 100644 --- a/cli/root.go +++ b/cli/root.go @@ -44,7 +44,7 @@ var rootCmd = &cobra.Command{ По умолчанию файл конфигурации ищется по пути: %s `, err, defaultConfigPath) - os.Exit(1) + return } config, err := cfg.ReadConfig(defaultConfigPath) @@ -70,7 +70,7 @@ var rootCmd = &cobra.Command{ func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "ошибка: %v\n", err) - os.Exit(1) + return } } diff --git a/cli/testyggdrasil.go b/cli/testyggdrasil.go index df3405e..eca78be 100644 --- a/cli/testyggdrasil.go +++ b/cli/testyggdrasil.go @@ -26,7 +26,7 @@ var testYggdrasilCmd = &cobra.Command{ v, err := cfg.LoadViperConfig(defaultConfigPath) if err != nil { fmt.Fprintf(os.Stderr, "не удалось прочитать конфигурацию viper: %v", err) - os.Exit(1) + return err } config, err := cfg.ReadConfig(defaultConfigPath) if err != nil { From f35c528ba65fa540751bdeb38125338cf6c9de52 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Thu, 18 Sep 2025 00:28:25 +0300 Subject: [PATCH 15/15] autocreate config dir fix --- cfg/configfunctions.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cfg/configfunctions.go b/cfg/configfunctions.go index b21d3b8..befb37d 100644 --- a/cfg/configfunctions.go +++ b/cfg/configfunctions.go @@ -256,10 +256,12 @@ func InitGenesis(chainName, defaultConfigPath string) (*cfg.Config, *viper.Viper config := cfg.DefaultConfig() config.RootDir = filepath.Dir(filepath.Dir(defaultConfigPath)) - // Создаём директорию, если её нет if err := os.MkdirAll(config.RootDir, 0o755); err != nil { return nil, nil, fmt.Errorf("failed to create config directory %s: %w", config.RootDir, err) } + if err := os.MkdirAll(filepath.Dir(defaultConfigPath), 0o755); err != nil { + return nil, nil, fmt.Errorf("failed to create config directory %s: %w", filepath.Dir(defaultConfigPath), err) + } nodeinfo := p2p.DefaultNodeInfo{} viper := WriteConfig(config, &defaultConfigPath, nodeinfo)