diff --git a/.gitignore b/.gitignore index 6f72f89..c61a73f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ go.work.sum # env file .env + +lbc_client +config/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c61b1e2 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module lbc_client + +go 1.24.3 + +require ( + github.com/google/uuid v1.6.0 + github.com/spf13/pflag v1.0.7 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b21c462 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +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/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= diff --git a/main.go b/main.go new file mode 100644 index 0000000..93ecf2a --- /dev/null +++ b/main.go @@ -0,0 +1,261 @@ +package main + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/google/uuid" + "github.com/spf13/pflag" +) + +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"` +} + +type CommitmentTxBody struct { + Type string `json:"type"` + ID string `json:"id"` + PromiseID string `json:"promise_id"` + CommiterID string `json:"commiter_id"` +} + +type SignedTx struct { + Body any `json:"body"` + Signature string `json:"signature"` +} + +type rpcResp struct { + JSONRPC string `json:"jsonrpc"` + ID any `json:"id"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + Data string `json:"data"` + } `json:"error"` + Result *struct { + CheckTx struct { + Code uint32 `json:"code"` + Log string `json:"log"` + } `json:"check_tx"` + DeliverTx struct { + Code uint32 `json:"code"` + Log string `json:"log"` + } `json:"deliver_tx"` + } `json:"result"` +} + +type CompoundTx struct { + Body struct { + Promise *PromiseTxBody `json:"promise"` + Commitment *CommitmentTxBody `json:"commitment"` + } `json:"body"` + Signature string `json:"signature"` +} + +const configDir = "./config" +const privKeyPath = configDir + "/ed25519.key" +const pubKeyPath = configDir + "/ed25519.pub" + +func parseRPCResult(data []byte) error { + var r rpcResp + _ = json.Unmarshal(data, &r) + if r.Error != nil { + return fmt.Errorf("RPC error: %d %s (%s)", r.Error.Code, r.Error.Message, r.Error.Data) + } + if r.Result == nil { + return fmt.Errorf("empty result") + } + if r.Result.CheckTx.Code != 0 { + return fmt.Errorf("CheckTx failed: %s", r.Result.CheckTx.Log) + } + if r.Result.DeliverTx.Code != 0 { + return fmt.Errorf("DeliverTx failed: %s", r.Result.DeliverTx.Log) + } + return nil +} + +func ensureKeypair() (ed25519.PublicKey, ed25519.PrivateKey, error) { + if _, err := os.Stat(privKeyPath); os.IsNotExist(err) { + fmt.Println("🔐 Generating new ed25519 keypair...") + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + os.MkdirAll(configDir, 0700) + os.WriteFile(privKeyPath, priv, 0600) + os.WriteFile(pubKeyPath, pub, 0644) + return pub, priv, nil + } + priv, _ := os.ReadFile(privKeyPath) + pub, _ := os.ReadFile(pubKeyPath) + return ed25519.PublicKey(pub), ed25519.PrivateKey(priv), nil +} + +func sendTx(tx SignedTx, rpcURL string) error { + txBytes, _ := json.Marshal(tx) + txB64 := base64.StdEncoding.EncodeToString(txBytes) + final := map[string]any{ + "jsonrpc": "2.0", + "id": uuid.NewString(), + "method": "broadcast_tx_commit", + "params": map[string]string{ + "tx": txB64, + }, + } + finalBytes, _ := json.Marshal(final) + fmt.Println("🚧 Raw tx JSON:", string(txBytes)) + fmt.Printf("🚧 Final RPC body:\n%s\n", string(finalBytes)) + resp, err := http.Post(rpcURL, "application/json", bytes.NewReader(finalBytes)) + if err != nil { + return err + } + defer resp.Body.Close() + data, _ := io.ReadAll(resp.Body) + fmt.Println("📡 Response:") + fmt.Println(string(data)) + if err := parseRPCResult(data); err != nil { + return err + } + return nil +} + +func registerCommiter(name, rpcURL string) error { + pub, priv, _ := ensureKeypair() + pubB64 := base64.StdEncoding.EncodeToString(pub) + id := "commiter:" + pubB64 + body := CommiterTxBody{ + Type: "commiter", + ID: id, + Name: name, + CommiterPubKey: pubB64, // ← ты думаешь, а на деле может быть иначе + } + bodyBytes, _ := json.Marshal(body) + sig := ed25519.Sign(priv, bodyBytes) + tx := SignedTx{Body: body, Signature: base64.StdEncoding.EncodeToString(sig)} + return sendTx(tx, rpcURL) +} + +func createPromise(desc, rpcURL string) error { + pub, priv, _ := ensureKeypair() + pubB64 := base64.StdEncoding.EncodeToString(pub) + + commiterID := "commiter:" + pubB64 + promiseID := "promise:" + uuid.NewString() + commitmentID := "commitment:" + uuid.NewString() + + // Объекты тела + promise := &PromiseTxBody{ + Type: "promise", + ID: promiseID, + Description: desc, + Timestamp: time.Now().Unix(), + } + commitment := &CommitmentTxBody{ + Type: "commitment", + ID: commitmentID, + PromiseID: promiseID, + CommiterID: commiterID, + } + + // Сборка тела для подписи + bodyStruct := struct { + Promise *PromiseTxBody `json:"promise"` + Commitment *CommitmentTxBody `json:"commitment"` + }{ + Promise: promise, + Commitment: commitment, + } + + // Сериализация и подпись + bodyBytes, err := json.Marshal(bodyStruct) + if err != nil { + return fmt.Errorf("failed to marshal compound body: %w", err) + } + sig := ed25519.Sign(priv, bodyBytes) + sigB64 := base64.StdEncoding.EncodeToString(sig) + + // Формирование транзакции + compound := CompoundTx{ + Body: bodyStruct, + Signature: sigB64, + } + + // Отправка + txBytes, err := json.Marshal(compound) + if err != nil { + return fmt.Errorf("failed to marshal compound tx: %w", err) + } + txB64 := base64.StdEncoding.EncodeToString(txBytes) + final := map[string]any{ + "jsonrpc": "2.0", + "id": uuid.NewString(), + "method": "broadcast_tx_commit", + "params": map[string]string{ + "tx": txB64, + }, + } + + finalBytes, _ := json.Marshal(final) + fmt.Printf("🚧 Final RPC body:\n%s\n", string(finalBytes)) + resp, err := http.Post(rpcURL, "application/json", bytes.NewReader(finalBytes)) + if err != nil { + return err + } + defer resp.Body.Close() + data, _ := io.ReadAll(resp.Body) + fmt.Println("📡 Response:") + fmt.Println(string(data)) + if err := parseRPCResult(data); err != nil { + return err + } + return nil +} + +func main() { + var name, desc, rpc string + pflag.StringVar(&name, "name", "", "register commiter name") + pflag.StringVar(&desc, "desc", "", "create promise description") + pflag.StringVar(&rpc, "rpc", "http://localhost:26657", "Tendermint RPC URL") + pflag.Parse() + + if name != "" { + err := registerCommiter(name, rpc) + if err != nil { + fmt.Fprintf(os.Stderr, "❌ Error: %v\n", err) + os.Exit(1) + } + fmt.Println("✅ Commiter registered successfully") + return + } + + if desc != "" { + err := createPromise(desc, rpc) + if err != nil { + fmt.Fprintf(os.Stderr, "❌ Error: %v\n", err) + os.Exit(1) + } + fmt.Println("✅ Promise and Commitment created successfully") + return + } + + fmt.Println("⛔ Please provide either --name or --desc") + os.Exit(1) +}