261 lines
6.5 KiB
Go
261 lines
6.5 KiB
Go
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)
|
||
}
|