diff --git a/blockchain/abci.go b/blockchain/abci.go
index 4ad3558..e90f342 100644
--- a/blockchain/abci.go
+++ b/blockchain/abci.go
@@ -1,67 +1,260 @@
package blockchain
import (
- "bytes"
+ "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 KVStoreApplication struct {
+type PromiseApp struct {
db *badger.DB
currentBatch *badger.Txn
}
-func NewKVStoreApplication(db *badger.DB) *KVStoreApplication {
- return &KVStoreApplication{db: db}
+type compoundTxRaw struct {
+ Body json.RawMessage `json:"body"`
+ Signature string `json:"signature"`
}
-// Проверка транзакции (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]
+type compoundBody struct {
+ Promise *types.PromiseTxBody `json:"promise"`
+ Commitment *types.CommitmentTxBody `json:"commitment"`
+}
- // Проверяем, существует ли уже такая запись
- 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
+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"}
}
- return item.Value(func(val []byte) error {
- if bytes.Equal(val, value) {
- return fmt.Errorf("duplicate key-value")
+ // 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
- })
- })
-
- if err != nil {
- if err.Error() == "duplicate key-value" {
- return 2 // дубликат найден
+ }); err != nil {
+ return abci.ResponseCheckTx{Code: 4, Log: err.Error()}
}
- // любая другая ошибка
- return 1
+
+ // Существование бенефициара
+ 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}
}
- return 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 *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 +262,222 @@ 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 {
+ // Попытка композита
+ 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}
}
- parts := bytes.Split(req.Tx, []byte("="))
- 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])
- if err != nil {
- return abci.ResponseDeliverTx{
- Code: 1,
- Log: fmt.Sprintf("Failed to set key: %v", err),
+ // Одиночный 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}
}
}
- 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 (app *KVStoreApplication) Commit() abci.ResponseCommit {
+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 {
- // 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}
+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()
+ 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 item.Value(func(val []byte) error {
- resp.Value = val
- return nil
- })
+ 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{},
- }
+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..e80d8a4 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"
@@ -23,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
@@ -63,7 +65,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 +112,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)
diff --git a/cfg/configfunctions.go b/cfg/configfunctions.go
index b4705d9..befb37d 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"
@@ -163,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)
@@ -250,18 +252,25 @@ 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)
+ }
+ 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)
}
- 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 77fab6d..03c6747 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"
)
@@ -17,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)
@@ -29,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 e582de6..303956c 100644
--- a/cli/root.go
+++ b/cli/root.go
@@ -3,13 +3,16 @@ package cli
import (
"context"
"fmt"
- "lbc/blockchain"
- "lbc/cfg"
- "lbc/yggdrasil"
+ "io"
"os"
"os/signal"
+ "path/filepath"
"syscall"
+ "github.com/gregorybednov/lbc/blockchain"
+ "github.com/gregorybednov/lbc/cfg"
+ "github.com/gregorybednov/lbc/yggdrasil"
+
"github.com/spf13/cobra"
)
@@ -41,7 +44,7 @@ var rootCmd = &cobra.Command{
По умолчанию файл конфигурации ищется по пути: %s
`, err, defaultConfigPath)
- os.Exit(1)
+ return
}
config, err := cfg.ReadConfig(defaultConfigPath)
@@ -50,7 +53,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)
@@ -67,6 +70,38 @@ var rootCmd = &cobra.Command{
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "ошибка: %v\n", err)
- os.Exit(1)
+ return
}
}
+
+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
+}
diff --git a/cli/testyggdrasil.go b/cli/testyggdrasil.go
index 58f8b01..eca78be 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"
)
@@ -25,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 {
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 @@
+
\ No newline at end of file
diff --git a/docs/database_schema.md b/docs/database_schema.md
new file mode 100644
index 0000000..00ba10c
--- /dev/null
+++ b/docs/database_schema.md
@@ -0,0 +1,43 @@
+# Логическая модель данных
+
+![[ER.svg]]
+
+
+ @startuml
+
+ entity Promise {
+ * ID: uuid
+ --
+ * text: string
+ due: datetime
+ BeneficiaryID: uuid
+ ParentPromiseID: uuid
+ }
+
+ entity Beneficiary {
+ * ID: uuid
+ --
+ * name: string
+ }
+
+ entity Commitment {
+ * ID: uuid
+ --
+ PromiseID: uuid
+ CommiterID: uuid
+ due: datetime
+ }
+
+ entity Commiter {
+ * ID: uuid
+ --
+ * name: string
+ }
+
+ Commitment }|--|| Promise : belongs to
+ Commitment }|--|| Commiter : made by
+ Promise }o--|| Beneficiary : has
+ Promise }--o Promise : parent of
+
+ @enduml
+
diff --git a/go.mod b/go.mod
index e9944ee..7b3f7b1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module lbc
+module github.com/gregorybednov/lbc
go 1.24.3
@@ -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-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 fbd0cf5..fca1357 100644
--- a/go.sum
+++ b/go.sum
@@ -195,6 +195,10 @@ 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/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=
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() {
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.