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 @@ +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..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.