2025-07-15 14:59:32 +03:00
|
|
|
|
package blockchain
|
2025-07-08 04:28:42 +03:00
|
|
|
|
|
|
|
|
|
|
import (
|
2025-08-10 10:22:19 +03:00
|
|
|
|
"crypto/ed25519"
|
|
|
|
|
|
"encoding/base64"
|
2025-08-05 13:02:14 +03:00
|
|
|
|
"encoding/json"
|
2025-08-10 10:22:19 +03:00
|
|
|
|
"errors"
|
2025-07-08 04:28:42 +03:00
|
|
|
|
"fmt"
|
2025-08-05 13:02:14 +03:00
|
|
|
|
"strings"
|
2025-07-08 04:28:42 +03:00
|
|
|
|
|
2025-08-10 13:37:06 +03:00
|
|
|
|
types "github.com/gregorybednov/lbc_sdk"
|
2025-08-10 12:22:09 +03:00
|
|
|
|
|
2025-07-08 04:28:42 +03:00
|
|
|
|
"github.com/dgraph-io/badger"
|
|
|
|
|
|
abci "github.com/tendermint/tendermint/abci/types"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-08-05 13:02:14 +03:00
|
|
|
|
type PromiseApp struct {
|
2025-07-08 04:28:42 +03:00
|
|
|
|
db *badger.DB
|
|
|
|
|
|
currentBatch *badger.Txn
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 10:22:19 +03:00
|
|
|
|
type compoundTxRaw struct {
|
|
|
|
|
|
Body json.RawMessage `json:"body"`
|
|
|
|
|
|
Signature string `json:"signature"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type compoundBody struct {
|
2025-08-10 12:17:27 +03:00
|
|
|
|
Promise *types.PromiseTxBody `json:"promise"`
|
|
|
|
|
|
Commitment *types.CommitmentTxBody `json:"commitment"`
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func NewPromiseApp(db *badger.DB) *PromiseApp {
|
|
|
|
|
|
return &PromiseApp{db: db}
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 22:39:28 +03:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-10 10:22:19 +03:00
|
|
|
|
func verifyAndExtractBody(db *badger.DB, tx []byte) (map[string]interface{}, error) {
|
|
|
|
|
|
var outer struct {
|
2025-08-10 12:17:27 +03:00
|
|
|
|
Body types.CommiterTxBody `json:"body"`
|
|
|
|
|
|
Signature string `json:"signature"`
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
2025-08-10 22:39:28 +03:00
|
|
|
|
// ---- Валидация содержимого композита по 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 {
|
2025-08-10 10:22:19 +03:00
|
|
|
|
return abci.ResponseCheckTx{Code: 2, Log: err.Error()}
|
|
|
|
|
|
}
|
2025-08-10 22:39:28 +03:00
|
|
|
|
if *p.ParentPromiseID == p.ID {
|
|
|
|
|
|
return abci.ResponseCheckTx{Code: 2, Log: "parent_promise_id must not equal promise id"}
|
|
|
|
|
|
}
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 22:39:28 +03:00
|
|
|
|
// Базовые обязательные поля 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
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
2025-08-10 22:39:28 +03:00
|
|
|
|
return errors.New("duplicate commitment ID")
|
|
|
|
|
|
}); err != nil {
|
|
|
|
|
|
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 22:39:28 +03:00
|
|
|
|
// Существование коммитера (у тебя уже было — оставляю)
|
|
|
|
|
|
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")
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
return nil
|
2025-08-10 22:39:28 +03:00
|
|
|
|
}); err != nil {
|
|
|
|
|
|
return abci.ResponseCheckTx{Code: 6, Log: err.Error()}
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return abci.ResponseCheckTx{Code: 0}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 22:39:28 +03:00
|
|
|
|
// ---- Попытка ОДИНОЧНЫХ транзакций ----
|
|
|
|
|
|
|
|
|
|
|
|
// 3.1) Совместимость: одиночный commiter (твоя старая логика)
|
2025-08-10 10:22:19 +03:00
|
|
|
|
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"}
|
|
|
|
|
|
}
|
2025-08-10 22:39:28 +03:00
|
|
|
|
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 {
|
2025-08-10 10:22:19 +03:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return errors.New("duplicate id")
|
2025-08-10 22:39:28 +03:00
|
|
|
|
}); 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 {
|
2025-08-10 10:22:19 +03:00
|
|
|
|
return abci.ResponseCheckTx{Code: 3, Log: err.Error()}
|
|
|
|
|
|
}
|
|
|
|
|
|
return abci.ResponseCheckTx{Code: 0}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 22:39:28 +03:00
|
|
|
|
// Если дошли сюда — составной формат не прошёл; вернём его причину.
|
2025-08-10 10:22:19 +03:00
|
|
|
|
return abci.ResponseCheckTx{Code: 1, Log: err.Error()}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) BeginBlock(_ abci.RequestBeginBlock) abci.ResponseBeginBlock {
|
2025-07-08 04:28:42 +03:00
|
|
|
|
if app.currentBatch != nil {
|
|
|
|
|
|
app.currentBatch.Discard()
|
|
|
|
|
|
}
|
|
|
|
|
|
app.currentBatch = app.db.NewTransaction(true)
|
|
|
|
|
|
return abci.ResponseBeginBlock{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx {
|
2025-08-10 22:39:28 +03:00
|
|
|
|
// Попытка композита
|
|
|
|
|
|
if compound, err := verifyCompoundTx(app.db, req.Tx); err == nil {
|
2025-08-10 10:22:19 +03:00
|
|
|
|
if app.currentBatch == nil {
|
|
|
|
|
|
app.currentBatch = app.db.NewTransaction(true)
|
|
|
|
|
|
}
|
2025-08-10 22:39:28 +03:00
|
|
|
|
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"}
|
|
|
|
|
|
}
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
2025-08-10 22:39:28 +03:00
|
|
|
|
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"}
|
|
|
|
|
|
}
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
|
|
|
|
|
return abci.ResponseDeliverTx{Code: 0}
|
|
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
|
2025-08-10 22:39:28 +03:00
|
|
|
|
// Одиночный 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}
|
|
|
|
|
|
}
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
2025-08-10 10:22:19 +03:00
|
|
|
|
|
2025-08-10 22:39:28 +03:00
|
|
|
|
// Одиночный beneficiary
|
|
|
|
|
|
{
|
|
|
|
|
|
var outer struct {
|
|
|
|
|
|
Body types.BeneficiaryTxBody `json:"body"`
|
|
|
|
|
|
Signature string `json:"signature"`
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
2025-08-10 22:39:28 +03:00
|
|
|
|
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}
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 22:39:28 +03:00
|
|
|
|
return abci.ResponseDeliverTx{Code: 1, Log: "invalid tx format"}
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 12:17:27 +03:00
|
|
|
|
func verifyCompoundTx(db *badger.DB, tx []byte) (*types.CompoundTx, error) {
|
2025-08-10 10:22:19 +03:00
|
|
|
|
// 1) Разобрать внешний конверт, body оставить сырым
|
|
|
|
|
|
var outerRaw compoundTxRaw
|
|
|
|
|
|
if err := json.Unmarshal(tx, &outerRaw); err != nil {
|
|
|
|
|
|
return nil, errors.New("invalid compound tx JSON")
|
2025-08-05 13:02:14 +03:00
|
|
|
|
}
|
2025-08-10 10:22:19 +03:00
|
|
|
|
if len(outerRaw.Body) == 0 {
|
|
|
|
|
|
return nil, errors.New("missing body")
|
2025-08-05 13:02:14 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 10:22:19 +03:00
|
|
|
|
// 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")
|
2025-08-05 13:02:14 +03:00
|
|
|
|
}
|
2025-08-10 10:22:19 +03:00
|
|
|
|
return item.Value(func(v []byte) error {
|
|
|
|
|
|
commiterData = append([]byte{}, v...)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
2025-08-05 13:02:14 +03:00
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
2025-08-10 10:22:19 +03:00
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2025-08-10 12:17:27 +03:00
|
|
|
|
var commiter types.CommiterTxBody
|
2025-08-10 10:22:19 +03:00
|
|
|
|
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)
|
2025-08-05 13:02:14 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-10 10:22:19 +03:00
|
|
|
|
// 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) Вернуть в привычной форме
|
2025-08-10 12:17:27 +03:00
|
|
|
|
return &types.CompoundTx{
|
2025-08-10 10:22:19 +03:00
|
|
|
|
Body: struct {
|
2025-08-10 12:17:27 +03:00
|
|
|
|
Promise *types.PromiseTxBody `json:"promise"`
|
|
|
|
|
|
Commitment *types.CommitmentTxBody `json:"commitment"`
|
2025-08-10 10:22:19 +03:00
|
|
|
|
}{
|
|
|
|
|
|
Promise: body.Promise,
|
|
|
|
|
|
Commitment: body.Commitment,
|
|
|
|
|
|
},
|
|
|
|
|
|
Signature: outerRaw.Signature,
|
|
|
|
|
|
}, nil
|
2025-08-05 13:02:14 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (app *PromiseApp) Commit() abci.ResponseCommit {
|
2025-07-08 04:28:42 +03:00
|
|
|
|
if app.currentBatch != nil {
|
|
|
|
|
|
err := app.currentBatch.Commit()
|
|
|
|
|
|
if err != nil {
|
2025-08-05 13:02:14 +03:00
|
|
|
|
fmt.Printf("Commit error: %v\n", err)
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
|
|
|
|
|
app.currentBatch = nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return abci.ResponseCommit{Data: []byte{}}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 13:02:14 +03:00
|
|
|
|
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] + ":")
|
2025-07-08 04:28:42 +03:00
|
|
|
|
|
2025-08-05 13:02:14 +03:00
|
|
|
|
var result []json.RawMessage
|
2025-07-08 04:28:42 +03:00
|
|
|
|
err := app.db.View(func(txn *badger.Txn) error {
|
2025-08-05 13:02:14 +03:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
return nil
|
2025-07-08 04:28:42 +03:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
2025-08-05 13:02:14 +03:00
|
|
|
|
return abci.ResponseQuery{Code: 1, Log: err.Error()}
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
all, _ := json.Marshal(result)
|
|
|
|
|
|
return abci.ResponseQuery{Code: 0, Value: all}
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) Info(req abci.RequestInfo) abci.ResponseInfo {
|
|
|
|
|
|
return abci.ResponseInfo{Data: "promises", Version: "0.1"}
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) SetOption(req abci.RequestSetOption) abci.ResponseSetOption {
|
|
|
|
|
|
return abci.ResponseSetOption{}
|
2025-07-08 04:28:42 +03:00
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) InitChain(req abci.RequestInitChain) abci.ResponseInitChain {
|
2025-07-08 04:28:42 +03:00
|
|
|
|
return abci.ResponseInitChain{}
|
|
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock {
|
2025-07-08 04:28:42 +03:00
|
|
|
|
return abci.ResponseEndBlock{}
|
|
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) ListSnapshots(req abci.RequestListSnapshots) abci.ResponseListSnapshots {
|
2025-07-08 04:28:42 +03:00
|
|
|
|
return abci.ResponseListSnapshots{}
|
|
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) OfferSnapshot(req abci.RequestOfferSnapshot) abci.ResponseOfferSnapshot {
|
2025-07-08 04:28:42 +03:00
|
|
|
|
return abci.ResponseOfferSnapshot{Result: abci.ResponseOfferSnapshot_REJECT}
|
|
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) LoadSnapshotChunk(req abci.RequestLoadSnapshotChunk) abci.ResponseLoadSnapshotChunk {
|
2025-07-08 04:28:42 +03:00
|
|
|
|
return abci.ResponseLoadSnapshotChunk{}
|
|
|
|
|
|
}
|
2025-08-05 13:02:14 +03:00
|
|
|
|
func (app *PromiseApp) ApplySnapshotChunk(req abci.RequestApplySnapshotChunk) abci.ResponseApplySnapshotChunk {
|
2025-07-08 04:28:42 +03:00
|
|
|
|
return abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT}
|
|
|
|
|
|
}
|