diff --git a/Makefile b/Makefile index d7aaacb..bd84c6c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ all: go build -o arbiter app/arbiter/main.go + go build -o keystore-generator app/keystore-generator/main.go linux: - GOARCH=amd64 GOOS=linux go build -o arbiter app/arbiter/main.go \ No newline at end of file + GOARCH=amd64 GOOS=linux go build -o arbiter app/arbiter/main.go + GOARCH=amd64 GOOS=linux go build -o keystore-generator app/keystore-generator/main.go \ No newline at end of file diff --git a/README.MD b/README.MD index bd53edc..824b8ab 100644 --- a/README.MD +++ b/README.MD @@ -68,28 +68,28 @@ $ make keyFilePath: "~/loan_arbiter/keys" ``` - - ### 3. prepare keystore to keyFilePath -1. create **btcKey.json** **escKey.json** and put it into **keyFilePath** +1. create **btcKey** **escKey** and put it into **keyFilePath** btcKey is used to sign request_arbitration_btc_tx escKey is used to submit arbitration signature to esc arbiter contract, gas fee(esc ELA) is needed -2. keysotre file need to set hex encoded private key - - btcKey.json - ```json - { - "privKey": "[HEX_PRIV_KEY]" - } - ``` - escKey.json - ```json - { - "privKey": "[HEX_PRIV_KEY]" - } - ``` +2. Use keystore-generator tool to create keystore files: + + Install keystore-generator: + ```shell + go install github.com/BeL2Labs/keystore-generator@latest + ``` + + Generate BTC keystore: + ```shell + keystore-generator -c btc -s YOUR_PRIVATE_KEY -p YOUR_PASSWORD -o btcKey + ``` + + Generate ESC keystore: + ```shell + keystore-generator -c eth -s YOUR_PRIVATE_KEY -p YOUR_PASSWORD -o escKey + ``` ### 4. run arbiter @@ -113,4 +113,4 @@ The automated deployment process can be referenced in the documentation: [deploy_loan_arbiter.md](https://github.com/BeL2Labs/Arbiter_Signer/blob/main/docs/deploy_loan_arbiter.md) ## License -arbiter signer is licensed under the [copyfree](http://copyfree.org) MIT License. \ No newline at end of file +arbiter signer is licensed under the [copyfree](http://copyfree.org) MIT License. diff --git a/app/arbiter/arbiter/arbiter.go b/app/arbiter/arbiter/arbiter.go index a33cb51..d6743b6 100644 --- a/app/arbiter/arbiter/arbiter.go +++ b/app/arbiter/arbiter/arbiter.go @@ -8,7 +8,6 @@ import ( "crypto/sha256" "encoding/gob" "encoding/hex" - "encoding/json" "log" "os" "path/filepath" @@ -30,6 +29,7 @@ import ( "github.com/BeL2Labs/Arbiter_Signer/app/arbiter/config" "github.com/BeL2Labs/Arbiter_Signer/app/arbiter/contract" "github.com/BeL2Labs/Arbiter_Signer/app/arbiter/contract/events" + "github.com/BeL2Labs/Arbiter_Signer/app/arbiter/crypto" ) const DELAY_BLOCK uint64 = 3 @@ -49,25 +49,21 @@ type Arbiter struct { logger *log.Logger } -func NewArbiter(ctx context.Context, config *config.Config) *Arbiter { - escData, err := os.ReadFile(config.EscKeyFilePath) +func NewArbiter(ctx context.Context, config *config.Config, password string) *Arbiter { + escPrivKey, err := crypto.GetEthKeyFromKeystore(config.EscKeyFilePath, password) if err != nil { g.Log().Fatal(ctx, "get esc keyfile error", err, " keystore path ", config.EscKeyFilePath) } - var escAccount account - err = json.Unmarshal(escData, &escAccount) - if err != nil { - g.Log().Fatal(ctx, "Unmarshal keyfile error", err, " content ", string(escData)) + escAccount := account{ + PrivateKey: escPrivKey, } - arbiterData, err := os.ReadFile(config.ArbiterKeyFilePath) + arbiterPrivKey, err := crypto.GetBtcKeyFromKeystore(config.ArbiterKeyFilePath, password) if err != nil { g.Log().Fatal(ctx, "get arbiter keyfile error", err, " keystore path ", config.ArbiterKeyFilePath) } - var arbiterAccount account - err = json.Unmarshal(arbiterData, &arbiterAccount) - if err != nil { - g.Log().Fatal(ctx, "Unmarshal keyfile error", err, " content ", string(arbiterData)) + arbiterAccount := account{ + PrivateKey: arbiterPrivKey, } err = createDir(config) @@ -114,17 +110,6 @@ func (v *Arbiter) listenESCContract() { startHeight = v.config.ESCStartHeight } - keyfile := v.config.EscKeyFilePath - data, err := os.ReadFile(keyfile) - if err != nil { - g.Log().Fatal(v.ctx, "get keyfile error", err, " private key path ", keyfile) - } - var a account - err = json.Unmarshal(data, &a) - if err != nil { - g.Log().Fatal(v.ctx, "Unmarshal keyfile error", err, " content ", string(data)) - } - v.escNode.Start(startHeight) } diff --git a/app/arbiter/crypto/keystore.go b/app/arbiter/crypto/keystore.go new file mode 100644 index 0000000..a6f8f53 --- /dev/null +++ b/app/arbiter/crypto/keystore.go @@ -0,0 +1,135 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "os" + + "github.com/ethereum/go-ethereum/accounts/keystore" +) + +// ReadBTCKeystore reads a BTC keystore file +func ReadBTCKeystore(path string) ([]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + // BTC keystore is binary format, return raw data + return data, nil +} + +// ReadETHKeystore reads an ETH keystore file +func ReadETHKeystore(path string) (*keystore.Key, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + // Parse ETH keystore JSON + var key keystore.Key + if err := json.Unmarshal(data, &key); err != nil { + return nil, err + } + + return &key, nil +} + +// ReadKeystore automatically detects and reads either BTC or ETH keystore +func ReadKeystore(path string) (interface{}, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + // Try to parse as ETH keystore first + var ethKey keystore.Key + if err := json.Unmarshal(data, ðKey); err == nil { + return ðKey, nil + } + + // If not ETH keystore, treat as BTC keystore + return data, nil +} + +// GetKeyType determines if a keystore is BTC or ETH format +func GetKeyType(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + + // Check if data is valid JSON + var temp interface{} + if json.Unmarshal(data, &temp) == nil { + return "eth", nil + } + + // Check if data is valid hex (BTC WIF) + if _, err := hex.DecodeString(string(data)); err == nil { + return "btc", nil + } + + return "", errors.New("unknown keystore format") +} + +// GetEthKeyFromKeystore reads an ETH keystore file and returns the private key as hex string +func GetEthKeyFromKeystore(path, password string) (string, error) { + // Read ETH keystore file + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + + // Decrypt ETH keystore + privateKey, err := keystore.DecryptKey(data, password) + if err != nil { + return "", err + } + return hex.EncodeToString(privateKey.PrivateKey.D.Bytes()), nil +} + +// GetBtcKeyFromKeystore reads a BTC keystore file and returns the private key as hex string +func GetBtcKeyFromKeystore(path, password string) (string, error) { + data, err := ReadBTCKeystore(path) + if err != nil { + return "", err + } + // Decrypt BTC keystore using password + decryptedData, err := decryptBTCKeystore(data, password) + if err != nil { + return "", err + } + return hex.EncodeToString(decryptedData), nil +} + +// decryptBTCKeystore decrypts BTC keystore data using password +func decryptBTCKeystore(data []byte, password string) ([]byte, error) { + key := sha256.Sum256([]byte(password)) + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, errors.New("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} diff --git a/app/arbiter/main.go b/app/arbiter/main.go index c132a75..57c8052 100644 --- a/app/arbiter/main.go +++ b/app/arbiter/main.go @@ -4,6 +4,7 @@ package main import ( "context" + "flag" "fmt" "os" "path/filepath" @@ -20,6 +21,19 @@ import ( ) func main() { + passwordPtr := flag.String("p", "", "Specify the password") + flag.Parse() + var password string + if *passwordPtr == "" { + fmt.Print("please input key password: ") + _, err := fmt.Scanln(&password) + if err != nil { + fmt.Println("read password error:", err) + os.Exit(1) + } + } else { + password = *passwordPtr + } if len(os.Args) > 1 { operation := os.Args[1] @@ -41,7 +55,7 @@ func main() { wg.Add(1) // start arbiter g.Log().Info(ctx, "Starting arbiter...") - arb := arbiter.NewArbiter(ctx, initConfig(ctx)) + arb := arbiter.NewArbiter(ctx, initConfig(ctx), password) arb.Start() wg.Wait() } @@ -116,8 +130,8 @@ func initConfig(ctx context.Context) *config.Config { g.Log().Info(ctx, "keyFilePath:", keyFilePath) // if want to submit to ESC contract successfully, need to use esc ela as gas. - escKeyFilePath := gfile.Join(keyFilePath, "escKey.json") - arbiterKeyFilePath := gfile.Join(keyFilePath, "btcKey.json") + escKeyFilePath := gfile.Join(keyFilePath, "escKey") + arbiterKeyFilePath := gfile.Join(keyFilePath, "btcKey") logPath := gfile.Join(dataPath, "logs/") loanPath := gfile.Join(dataPath, "loan/") loanNeedSignReqPath := gfile.Join(loanPath, "request/") diff --git a/app/keystore-generator/btc/btc.go b/app/keystore-generator/btc/btc.go new file mode 100644 index 0000000..60be5ad --- /dev/null +++ b/app/keystore-generator/btc/btc.go @@ -0,0 +1,75 @@ +// Copyright (c) 2025 The bel2 developers +package btc + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "errors" + "fmt" + "io" + "os" +) + +func ParseKeystore(filePath string, password string) (string, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("failed to read keystore file: %v", err) + } + + // Decrypt the data + decrypted, err := decrypt(data, password) + if err != nil { + return "", fmt.Errorf("failed to decrypt keystore: %v", err) + } + + return string(decrypted), nil +} + +func Encrypt(data []byte, password string) ([]byte, error) { + key := sha256.Sum256([]byte(password)) + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + ciphertext := gcm.Seal(nonce, nonce, data, nil) + return ciphertext, nil +} + +func decrypt(data []byte, password string) ([]byte, error) { + key := sha256.Sum256([]byte(password)) + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, errors.New("ciphertext too short") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} diff --git a/app/keystore-generator/eth/eth.go b/app/keystore-generator/eth/eth.go new file mode 100644 index 0000000..d24996c --- /dev/null +++ b/app/keystore-generator/eth/eth.go @@ -0,0 +1,29 @@ +// Copyright (c) 2025 The bel2 developers +package eth + +import ( + "fmt" + "os" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +func ParseKeystore(filePath string, password string) (string, error) { + // Read keystore file + data, err := os.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("failed to read keystore file: %v", err) + } + + // Decrypt keystore + key, err := keystore.DecryptKey(data, password) + if err != nil { + return "", fmt.Errorf("failed to decrypt keystore: %v", err) + } + + // Get private key + privateKeyBytes := crypto.FromECDSA(key.PrivateKey) + return common.Bytes2Hex(privateKeyBytes), nil +} diff --git a/app/keystore-generator/main.go b/app/keystore-generator/main.go new file mode 100644 index 0000000..e1ade72 --- /dev/null +++ b/app/keystore-generator/main.go @@ -0,0 +1,149 @@ +// Copyright (c) 2025 The bel2 developers +package main + +import ( + "bufio" + "encoding/hex" + "flag" + "fmt" + "os" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/BeL2Labs/Arbiter_Signer/app/keystore-generator/btc" + "github.com/BeL2Labs/Arbiter_Signer/app/keystore-generator/eth" +) + +func main() { + chain := flag.String("c", "eth", "Chain type (eth or btc)") + privateKey := flag.String("s", "", "Private key (64 hex characters)") + password := flag.String("p", "", "Password for keystore") + outputFile := flag.String("o", "", "Output filename (optional)") + fileToParse := flag.String("f", "", "Keystore file to parse") + flag.Parse() + + if *fileToParse != "" { + privateKey, err := eth.ParseKeystore(*fileToParse, *password) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + fmt.Println("Private key:", privateKey) + } else { + switch *chain { + case "eth": + GenerateETHKeystore(*privateKey, *password, *outputFile) + case "btc": + GenerateBTCKeystore(*privateKey, *password, *outputFile) + default: + fmt.Println("Error: Invalid chain type") + os.Exit(1) + } + } +} + +func GenerateETHKeystore(privateKeyHex string, password string, outputFile string) { + if privateKeyHex == "" { + fmt.Println("Error: Private key is required") + os.Exit(1) + } + + if len(privateKeyHex) != 64 { + fmt.Println("Error: Private key must be exactly 64 hex characters") + os.Exit(1) + } + + if password == "" { + fmt.Print("Enter password: ") + reader := bufio.NewReader(os.Stdin) + inputPassword, _ := reader.ReadString('\n') + password = inputPassword[:len(inputPassword)-1] + } + + privateKeyBytes, err := hex.DecodeString(privateKeyHex) + if err != nil { + fmt.Println("Error: Invalid private key format - must be 64 hex characters") + os.Exit(1) + } + + privateKey, err := crypto.ToECDSA(privateKeyBytes) + if err != nil { + fmt.Println("Error: Failed to create private key:", err) + os.Exit(1) + } + + ks := keystore.NewKeyStore(".", keystore.StandardScryptN, keystore.StandardScryptP) + + account, err := ks.ImportECDSA(privateKey, password) + if err != nil { + fmt.Println("Error: Failed to import private key:", err) + os.Exit(1) + } + + if outputFile != "" { + newPath := fmt.Sprintf("%s", outputFile) + err := os.Rename(account.URL.Path, newPath) + if err != nil { + fmt.Println("Error: Failed to rename keystore file:", err) + os.Exit(1) + } + fmt.Println("Ethereum keystore created successfully at:", newPath) + } else { + fmt.Println("Ethereum keystore created successfully at:", account.URL.Path) + } +} + +func GenerateBTCKeystore(privateKeyHex string, password string, outputFile string) { + if privateKeyHex == "" { + fmt.Println("Error: Private key is required") + os.Exit(1) + } + + if len(privateKeyHex) != 64 { + fmt.Println("Error: Private key must be exactly 64 hex characters") + os.Exit(1) + } + + if password == "" { + fmt.Print("Enter password: ") + reader := bufio.NewReader(os.Stdin) + inputPassword, _ := reader.ReadString('\n') + password = inputPassword[:len(inputPassword)-1] + } + + privateKeyBytes, err := hex.DecodeString(privateKeyHex) + if err != nil { + fmt.Println("Error: Invalid private key format - must be 64 hex characters") + os.Exit(1) + } + + privKey, _ := btcec.PrivKeyFromBytes(privateKeyBytes) + wif, err := btcutil.NewWIF(privKey, &chaincfg.MainNetParams, true) + if err != nil { + fmt.Println("Error: Failed to create Bitcoin WIF:", err) + os.Exit(1) + } + + filename := fmt.Sprintf("btc_keystore_%s.txt", wif.String()[:8]) + if outputFile != "" { + filename = outputFile + } + + // Encrypt WIF with password + encrypted, err := btc.Encrypt([]byte(wif.String()), password) + if err != nil { + fmt.Println("Error: Failed to encrypt keystore:", err) + os.Exit(1) + } + + if err := os.WriteFile(filename, encrypted, 0600); err != nil { + fmt.Println("Error: Failed to save Bitcoin keystore:", err) + os.Exit(1) + } + + fmt.Println("Bitcoin keystore created successfully at:", filename) +} diff --git a/docker/docker_run_arbiter_signer.sh b/docker/docker_run_arbiter_signer.sh index 078927e..a8c922f 100644 --- a/docker/docker_run_arbiter_signer.sh +++ b/docker/docker_run_arbiter_signer.sh @@ -2,9 +2,11 @@ read -p "please input Arbiter address: " arbiter_address read -p "please input Arbiter btc private key: " arbiter_btc_private_key read -p "please input Arbiter esc private key: " arbiter_esc_private_key +read -p "please set keystore password: " keystore_password docker run -d \ -e ARBITER_BTC_PRIVATE_KEY="$arbiter_btc_private_key" \ -e ARBITER_ESC_PRIVATE_KEY="$arbiter_esc_private_key" \ -e ARBITER_ADDRESS="$arbiter_address" \ - mollkeith/arbiter-signer:v0.0.1 \ No newline at end of file + -e ARBITER_KEYPASS="$keystore_password" \ + mollkeith/arbiter-signer:v0.0.2 \ No newline at end of file diff --git a/docker/dockerfile b/docker/dockerfile index 6c23c66..316c86e 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -6,6 +6,5 @@ RUN cd ~ RUN wget https://download.bel2.org/loan-arbiter/deploy_loan_arbiter.sh RUN chmod a+x deploy_loan_arbiter.sh RUN echo $ARBITER_ADDRESS -RUN echo $ARBITER_BTC_PRIVATE_KEY -RUN echo $ARBITER_ESC_PRIVATE_KEY -ENTRYPOINT ["/bin/bash", "-c", "./deploy_loan_arbiter.sh \"$ARBITER_ADDRESS\" \"$ARBITER_BTC_PRIVATE_KEY\" \"$ARBITER_ESC_PRIVATE_KEY\"; tail -f /dev/null"] +RUN echo $ARBITER_KEYPASS +ENTRYPOINT ["/bin/bash", "-c", "./deploy_loan_arbiter.sh \"$ARBITER_ADDRESS\" \"$ARBITER_BTC_PRIVATE_KEY\" \"$ARBITER_ESC_PRIVATE_KEY\" \"$ARBITER_KEYPASS\"; tail -f /dev/null"] diff --git a/docs/deploy_loan_arbiter.md b/docs/deploy_loan_arbiter.md index 1acde31..62c81dc 100644 --- a/docs/deploy_loan_arbiter.md +++ b/docs/deploy_loan_arbiter.md @@ -20,7 +20,7 @@ 5. Execute deploy script ```shell - ./deploy_loan_arbiter.sh [your_arbiter_esc_address] [hex_encoded_btc_private_key] [hex_encoded_esc_private_key] + ./deploy_loan_arbiter.sh [your_arbiter_esc_address] [hex_encoded_btc_private_key] [hex_encoded_esc_private_key] [your_keystore_password] ``` replace ***[your_arbiter_esc_address]*** with your esc arbiter address, not operator address, with "0x" at the begining. @@ -28,9 +28,11 @@ replace ***[hex_encoded_esc_private_key]*** with your own esc operator private key, without "0x" at the begining. + replace ***[your_keystore_password]*** with your own keystore password. + For example: ```shell - ./deploy_loan_arbiter.sh 0x0262aB0ED65373cC855C34529fDdeAa0e686D913 0123456789abcdef015522dd7fee2104750cb5c0be9d06d42348cf9b65c253cb0 0123456789abcdef015522dd7fee2104750cb5c0be9d06d42348cf9b65c253cb0 + ./deploy_loan_arbiter.sh 0x0262aB0ED65373cC855C34529fDdeAa0e686D913 0123456789abcdef015522dd7fee2104750cb5c0be9d06d42348cf9b65c253cb0 0123456789abcdef015522dd7fee2104750cb5c0be9d06d42348cf9b65c253cb0 mypassword ``` esc private key used to submit arbiter signature to esc contract, need to have enough ESC ELA! diff --git a/docs/deploy_loan_arbiter.sh b/docs/deploy_loan_arbiter.sh index 9f2a575..6aa85ec 100644 --- a/docs/deploy_loan_arbiter.sh +++ b/docs/deploy_loan_arbiter.sh @@ -81,10 +81,12 @@ deploy_arbiter() #mv conf/config.yaml . sed -i "s/0x0262aB0ED65373cC855C34529fDdeAa0e686D913/$1/g" config.yaml - #prepare key json - mv btcKey.json escKey.json keys/ - sed -i "s/hex_encoded_private_key/$2/g" keys/btcKey.json - sed -i "s/hex_encoded_private_key/$3/g" keys/escKey.json + #prepare keystore + # Generate BTC keystore + keystore-generator -c btc -s $2 -p $4 -o btcKey + # Generate ESC keystore + keystore-generator -c eth -s $3 -p $4 -o escKey + mv btcKey escKey keys/ #prepare arbiter if [ "$(uname -m)" == "armv6l" ] || [ "$(uname -m)" == "armv7l" ] || [ "$(uname -m)" == "aarch64" ]; then @@ -95,7 +97,7 @@ deploy_arbiter() echo_info "Replacing arbiter.." cp -v loan-arbiter-linux-arm64/arbiter ~/loan_arbiter/ echo_info "Starting arbtier..." - ./arbiter --gf.gcfg.file=config.yaml > $SCRIPT_PATH/data/logs/arbiter.log 2>&1 & + ./arbiter --gf.gcfg.file=config.yaml -p $4 > $SCRIPT_PATH/data/logs/arbiter.log 2>&1 & #rm -f loan-arbiter-linux-arm64.tgz conf.tgz else @@ -106,7 +108,7 @@ deploy_arbiter() echo_info "Replacing arbiter.." cp -v loan-arbiter-linux-x86_64/arbiter ~/loan_arbiter/ echo_info "Starting arbtier..." - ./arbiter --gf.gcfg.file=config.yaml > $SCRIPT_PATH/data/logs/arbiter.log 2>&1 & + ./arbiter --gf.gcfg.file=config.yaml -p $4 > $SCRIPT_PATH/data/logs/arbiter.log 2>&1 & #rm -f loan-arbiter-linux-x86_64.tgz conf.tgz fi @@ -116,4 +118,4 @@ deploy_arbiter() } SCRIPT_PATH=~/loan_arbiter -deploy_arbiter $1 $2 $3 +deploy_arbiter $1 $2 $3 $4