diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d090edb..21a72fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: branches: ["*"] env: - GO_VERSION: "1.24" + GO_VERSION: "1.25.5" jobs: test: @@ -49,13 +49,11 @@ jobs: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clean Go build cache - run: go clean -cache -modcache - - name: Run golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: version: latest + install-mode: goinstall args: --timeout=5m # Security vulnerability scanning @@ -101,8 +99,17 @@ jobs: fi continue-on-error: true + - name: Clean SARIF file (remove duplicate tags) + if: always() + run: | + # Remove duplicate tags from SARIF rules to fix validation errors + jq '(.runs[]?.tool.driver.rules[]?.properties.tags) |= unique' \ + govulncheck-results.sarif > govulncheck-results-clean.sarif + mv govulncheck-results-clean.sarif govulncheck-results.sarif + echo "✅ Cleaned govulncheck SARIF file" + - name: Upload govulncheck results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 if: always() with: sarif_file: govulncheck-results.sarif @@ -116,8 +123,17 @@ jobs: gosec -fmt sarif -out gosec-results.sarif -exclude G304 ./... continue-on-error: true + - name: Clean gosec SARIF file (remove duplicate tags) + if: always() + run: | + # Remove duplicate tags from SARIF rules to fix validation errors + jq '(.runs[]?.tool.driver.rules[]?.properties.tags) |= unique' \ + gosec-results.sarif > gosec-results-clean.sarif + mv gosec-results-clean.sarif gosec-results.sarif + echo "✅ Cleaned gosec SARIF file" + - name: Upload gosec results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 if: always() with: sarif_file: gosec-results.sarif @@ -151,7 +167,7 @@ jobs: run: go mod download - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} queries: +security-and-quality @@ -162,7 +178,7 @@ jobs: go build -v ./cmd/mpcium-cli - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" @@ -237,7 +253,7 @@ jobs: continue-on-error: true - name: Upload Grype results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 if: always() with: sarif_file: grype-results.sarif diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index e3443f3..e35378d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -54,7 +54,7 @@ jobs: needs: build strategy: matrix: - testcase: [TestKeyGeneration, TestSigning, TestResharing] + testcase: [TestKeyGeneration, TestSigning, TestResharing, TestCKDSigning] steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index cdc1fb9..90d1883 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ event_initiator.key event_initiator.key.age coverage.out coverage.html +node*/ peers.json # E2E test artifacts @@ -23,3 +24,4 @@ node2 config.yaml .vscode .vagrant +.chain_code \ No newline at end of file diff --git a/INSTALLATION.md b/INSTALLATION.md index a0754d2..8c95c7a 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -4,7 +4,7 @@ Before starting, ensure you have: -- **Go** 1.23+ installed: [Install Go here](https://go.dev/doc/install) +- **Go** 1.25.0+ installed: [Install Go here](https://go.dev/doc/install) - **NATS** server running - **Consul** server running @@ -41,14 +41,44 @@ go install ./cmd/mpcium-cli --- -### Set everything up in one go +## Setup Instructions + +**For detailed step-by-step instructions, see [SETUP.md](SETUP.md).** + +### Quick Reference + +#### 1. Generate peers.json + +First, generate the peers configuration file: + +```bash +mpcium-cli generate-peers -n 3 +``` + +This creates a `peers.json` file with 3 peer nodes (node0, node1, node2). Adjust `-n` for a different number of nodes. + +#### 2. Set up Event Initiator + +```bash +./setup_initiator.sh +``` + +This generates the event initiator identity used to authorize MPC operations. + +#### 3. Set up Node Identities ```bash -chmod +x ./setup.sh -./setup.sh +./setup_identities.sh ``` -Detailed steps can be found in [SETUP.md](SETUP.md). +This script: + +- Creates node directories (node0, node1, node2) +- Generates identities for each node +- Distributes identity files across nodes +- Configures chain_code for all nodes + +**Note:** This script requires `peers.json` to exist. If you see an error about missing peers.json, run step 1 first. --- @@ -56,6 +86,58 @@ Detailed steps can be found in [SETUP.md](SETUP.md). --- +## chain_code setup (REQUIRED) + +### What is chain_code? + +The `chain_code` is a cryptographic parameter used for Hierarchical Deterministic (HD) wallet functionality. It enables mpcium to derive child keys from a parent key, allowing you to generate multiple wallet addresses from a single master key. + +**Important Requirements:** + +- **All nodes in your MPC cluster MUST use the identical chain_code value** +- Must be a 32-byte value represented as a 64-character hexadecimal string +- Should be generated once and stored securely +- Without a valid chain_code, mpcium nodes will fail to start + +### How to generate and configure + +Generate one 32-byte hex chain code and set it in all node configurations: + +```bash +# Navigate to your mpcium directory +cd /path/to/mpcium + +# Generate a random 32-byte chain code and save it +CC=$(openssl rand -hex 32) && echo "$CC" > .chain_code + +# Apply to main config +sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" config.yaml + +# Apply to all node configs +for n in node0 node1 node2; do + sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" "$n/config.yaml" +done + +# Verify it was set correctly +echo "Chain code configured: $CC" +``` + +**Example config.yaml entry:** + +```yaml +chain_code: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" +``` + +Start nodes normally: + +```bash +cd node0 && mpcium start -n node0 +``` + +Repeat for `node1` and `node2`. The value must be exactly 64 hex chars (32 bytes). + +--- + ## Production Deployment (High Security) 1. Use production-grade **NATS** and **Consul** clusters. diff --git a/README.md b/README.md index fad562a..5fa615b 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,13 @@ The application uses a YAML configuration file (`config.yaml`) with the followin - `event_initiator_pubkey`: Public key of the event initiator - `max_concurrent_keygen`: Maximum concurrent key generation operations +#### chain_code (REQUIRED) +- **Required** for Hierarchical Deterministic (HD) wallet functionality to derive child keys +- Must be a 32-byte hexadecimal string (64 characters) +- **All nodes MUST use the exact same chain_code value** +- Generate with: `openssl rand -hex 32` +- See [INSTALLATION.md](./INSTALLATION.md#chain_code-setup-required) for detailed setup instructions + ## Installation - **Local Development**: For quick setup and testing, see [INSTALLATION.md](./INSTALLATION.md) diff --git a/cmd/mpcium/main.go b/cmd/mpcium/main.go index 85383f9..3c2c1a0 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -203,6 +203,12 @@ func runNode(ctx context.Context, c *cli.Command) error { peerNodeIDs := GetPeerIDs(peers) peerRegistry := mpc.NewRegistry(nodeID, peerNodeIDs, consulClient.KV(), directMessaging, pubsub, identityStore) + chainCodeHex := viper.GetString("chain_code") + ckd, err := mpc.NewCKDFromHex(chainCodeHex) + if err != nil { + logger.Fatal("Failed to create ckd store", err) + } + mpcNode := mpc.NewNode( nodeID, peerNodeIDs, @@ -212,6 +218,7 @@ func runNode(ctx context.Context, c *cli.Command) error { keyinfoStore, peerRegistry, identityStore, + ckd, ) defer mpcNode.Close() @@ -443,6 +450,14 @@ func checkRequiredConfigValues(appConfig *config.AppConfig) { if viper.GetString("event_initiator_pubkey") == "" { logger.Fatal("Event initiator public key is required", nil) } + + chainCode := strings.TrimSpace(viper.GetString("chain_code")) + if chainCode == "" { + logger.Fatal("chain_code is required in config.yaml", nil) + } + if len(chainCode) != 64 { // 32 bytes hex + logger.Fatal("chain_code must be 32-byte hex (64 chars)", nil) + } } func NewConsulClient(addr string) *api.Client { diff --git a/config.prod.yaml.template b/config.prod.yaml.template index e91f9e9..0f406d4 100644 --- a/config.prod.yaml.template +++ b/config.prod.yaml.template @@ -17,7 +17,14 @@ mpc_threshold: 1 environment: production # Set to production for production environment backup_enabled: true event_initiator_pubkey: "" -event_initiator_algorithm: ed25519 # ed25519 or p256 +event_initiator_algorithm: ed25519 # ed25519 or p256 + +# Chain Code for HD Wallet Child Key Derivation (REQUIRED) +# This is used for hierarchical deterministic (HD) wallet functionality to derive child keys. +# All nodes in the MPC cluster MUST use the same chain_code value. +# Generate once with: openssl rand -hex 32 +# Store securely and use the same value across all nodes +chain_code: "" backup_period_seconds: 300 # Seconds backup_dir: backups max_concurrent_keygen: 2 diff --git a/config.yaml.template b/config.yaml.template index d9f6e60..f99bcc8 100644 --- a/config.yaml.template +++ b/config.yaml.template @@ -8,6 +8,13 @@ environment: development badger_password: "F))ysJp?E]ol&I;^" event_initiator_algorithm: "ed25519" # or "ed25519", default: ed25519 event_initiator_pubkey: "event_initiator_pubkey" + +# Chain Code for HD Wallet Child Key Derivation (REQUIRED) +# This is used for hierarchical deterministic (HD) wallet functionality to derive child keys. +# All nodes in the MPC cluster MUST use the same chain_code value. +# Generate once with: openssl rand -hex 32 +# Example: chain_code: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" +chain_code: "" db_path: "." backup_enabled: true backup_period_seconds: 300 # 5 minutes diff --git a/deployments/systemd/setup-config.sh b/deployments/systemd/setup-config.sh index 30041f5..2e6415e 100755 --- a/deployments/systemd/setup-config.sh +++ b/deployments/systemd/setup-config.sh @@ -418,6 +418,27 @@ validate_config_credentials() { else log_info "✓ event_initiator_pubkey configured" fi + + # Check for required chain_code + if ! grep -q "^chain_code:" "$config_file" || grep -q "^chain_code: *$" "$config_file" || grep -q '^chain_code: ""' "$config_file"; then + log_error "❌ chain_code not configured in config.yaml" + log_error " Generate with: openssl rand -hex 32" + log_error " All nodes MUST use the same chain_code value" + ((errors++)) + else + # Validate chain_code is 64 hex characters (32 bytes) + local chain_code=$(grep "^chain_code:" "$config_file" | sed 's/chain_code: *//g' | sed 's/"//g' | sed "s/'//g" | sed 's/#.*//g' | sed 's/ *$//g') + if [[ ${#chain_code} -ne 64 ]]; then + log_error "❌ chain_code must be 64 hex characters (32 bytes), got ${#chain_code} characters" + log_error " Generate with: openssl rand -hex 32" + ((errors++)) + elif ! [[ "$chain_code" =~ ^[0-9a-fA-F]{64}$ ]]; then + log_error "❌ chain_code must be hexadecimal (0-9, a-f), got invalid characters" + ((errors++)) + else + log_info "✓ chain_code configured (${#chain_code} hex chars)" + fi + fi # Check for NATS configuration local nats_url=$(grep -A 10 "^nats:" "$config_file" | grep "url:" | sed 's/.*url: *//g' | sed 's/"//g' | sed "s/'//g" | sed 's/#.*//g' | sed 's/ *$//g') diff --git a/e2e/config.test.yaml.template b/e2e/config.test.yaml.template index 8d37279..910ae6c 100644 --- a/e2e/config.test.yaml.template +++ b/e2e/config.test.yaml.template @@ -11,3 +11,7 @@ nats: max_concurrent_keygen: 1 max_concurrent_signing: 10 session_warm_up_delay_ms: 500 + +# Chain Code for HD Wallet Child Key Derivation (REQUIRED) +# All nodes MUST use the same chain_code value +chain_code: "{{.CKDChainCode}}" diff --git a/e2e/go.mod b/e2e/go.mod index 7b6e43b..c763f77 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -106,3 +106,5 @@ require ( replace github.com/fystack/mpcium => ../ replace github.com/agl/ed25519 => github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43 + +replace github.com/bnb-chain/tss-lib/v2 => github.com/fystack/tss-lib/v2 v2.0.1 diff --git a/e2e/go.sum b/e2e/go.sum index 62e5338..9da8a63 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -53,8 +53,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43 h1:Vkf7rtHx8uHx8gDfkQaCdVfc+gfrF9v6sR6xJy7RXNg= github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43/go.mod h1:TnVqVdGEK8b6erOMkcyYGWzCQMw7HEMCOw3BgFYCFWs= -github.com/bnb-chain/tss-lib/v2 v2.0.2 h1:dL2GJFCSYsYQ0bHkGll+hNM2JWsC1rxDmJJJQEmUy9g= -github.com/bnb-chain/tss-lib/v2 v2.0.2/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= @@ -118,6 +116,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fystack/tss-lib/v2 v2.0.1 h1:xnC2+DYShoVWco1geliW0km9IvGD7T2FqFOeXM3/7K0= +github.com/fystack/tss-lib/v2 v2.0.1/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= diff --git a/e2e/setup_test_identities.sh b/e2e/setup_test_identities.sh index 07058e8..00c2dfa 100755 --- a/e2e/setup_test_identities.sh +++ b/e2e/setup_test_identities.sh @@ -28,6 +28,11 @@ echo "🔐 Generating random password for badger encryption..." BADGER_PASSWORD=$(< /dev/urandom tr -dc 'A-Za-z0-9' | head -c 32) echo "✅ Generated password: $BADGER_PASSWORD" +# Generate chain_code (32-byte hex value, 64 hex characters) +echo "🔐 Generating chain_code (32-byte hex)..." +CHAIN_CODE=$(openssl rand -hex 32) +echo "✅ Generated chain_code: $CHAIN_CODE" + # Generate config.test.yaml from template echo "📝 Generating config.test.yaml from template..." if [ ! -f "config.test.yaml.template" ]; then @@ -43,6 +48,7 @@ ESCAPED_PASSWORD=$(printf '%s\n' "$BADGER_PASSWORD" | sed 's/[[\.*^$()+?{|]/\\&/ sed -e "s/{{\.BadgerPassword}}/$ESCAPED_PASSWORD/g" \ -e "s/{{\.EventInitiatorPubkey}}/$TEMP_PUBKEY/g" \ + -e "s/{{\.CKDChainCode}}/$CHAIN_CODE/g" \ config.test.yaml.template > config.test.yaml echo "✅ Generated config.test.yaml from template" @@ -106,20 +112,35 @@ if [ -f "test_event_initiator.identity.json" ]; then PUBKEY=$(cat test_event_initiator.identity.json | jq -r '.public_key') echo "📝 Updating config files with event initiator public key and password..." - # Update all test node config files with the actual public key and password + # Update all test node config files with the actual public key, password, and chain_code for i in $(seq 0 $((NUM_NODES-1))); do # Update public key using sed with | as delimiter (safer than /) sed_inplace "s|event_initiator_pubkey:.*|event_initiator_pubkey: $PUBKEY|g" "$BASE_DIR/test_node$i/config.yaml" # Update password using sed with | as delimiter and escaped password sed_inplace "s|badger_password:.*|badger_password: $ESCAPED_PASSWORD|g" "$BASE_DIR/test_node$i/config.yaml" + # Update chain_code + if grep -q '^\s*chain_code:' "$BASE_DIR/test_node$i/config.yaml"; then + sed_inplace "s|chain_code:.*|chain_code: \"$CHAIN_CODE\"|g" "$BASE_DIR/test_node$i/config.yaml" + else + printf '\nchain_code: "%s"\n' "$CHAIN_CODE" >> "$BASE_DIR/test_node$i/config.yaml" + fi done # Also update the main config.test.yaml sed_inplace "s|event_initiator_pubkey:.*|event_initiator_pubkey: $PUBKEY|g" "$BASE_DIR/config.test.yaml" sed_inplace "s|badger_password:.*|badger_password: $ESCAPED_PASSWORD|g" "$BASE_DIR/config.test.yaml" + # Update chain_code in config.test.yaml if it was replaced with placeholder + if grep -q '{{\.CKDChainCode}}' "$BASE_DIR/config.test.yaml" 2>/dev/null; then + sed_inplace "s|{{\.CKDChainCode}}|$CHAIN_CODE|g" "$BASE_DIR/config.test.yaml" + elif grep -q '^\s*chain_code:' "$BASE_DIR/config.test.yaml"; then + sed_inplace "s|chain_code:.*|chain_code: \"$CHAIN_CODE\"|g" "$BASE_DIR/config.test.yaml" + else + printf '\nchain_code: "%s"\n' "$CHAIN_CODE" >> "$BASE_DIR/config.test.yaml" + fi echo "✅ Event initiator public key updated: $PUBKEY" echo "✅ Badger password updated: $BADGER_PASSWORD" + echo "✅ Chain code updated: $CHAIN_CODE" else echo "❌ Failed to generate event initiator identity" exit 1 diff --git a/e2e/sign_ckd_test.go b/e2e/sign_ckd_test.go new file mode 100644 index 0000000..63a9444 --- /dev/null +++ b/e2e/sign_ckd_test.go @@ -0,0 +1,338 @@ +package e2e + +import ( + "fmt" + "testing" + "time" + + "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/logger" + "github.com/fystack/mpcium/pkg/types" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCKDSigning(t *testing.T) { + suite := NewE2ETestSuite(".") + logger.Init("dev", true) + + // Comprehensive cleanup before starting tests + t.Log("Performing pre-test cleanup...") + suite.CleanupTestEnvironment(t) + + // Ensure cleanup happens even if test fails + defer func() { + t.Log("Performing post-test cleanup...") + suite.Cleanup(t) + }() + + // Setup infrastructure + t.Run("Setup", func(t *testing.T) { + // Run make clean first to ensure a clean build + t.Log("Running make clean to ensure clean build...") + err := suite.RunMakeClean() + require.NoError(t, err, "Failed to run make clean") + t.Log("make clean completed") + + t.Log("Starting setupInfrastructure...") + suite.SetupInfrastructure(t) + t.Log("setupInfrastructure completed") + + t.Log("Starting setupTestNodes...") + suite.SetupTestNodes(t) + t.Log("setupTestNodes completed") + + // Load config after setup script runs + err = suite.LoadConfig() + require.NoError(t, err, "Failed to load config after setup") + + t.Log("Starting registerPeers...") + suite.RegisterPeers(t) + t.Log("registerPeers completed") + + t.Log("Starting setupMPCClient...") + suite.SetupMPCClient(t) + t.Log("setupMPCClient completed") + + t.Log("Starting startNodes...") + suite.StartNodes(t) + t.Log("startNodes completed") + }) + + // Test key generation first + t.Run("KeyGenerationForSigning", func(t *testing.T) { + testKeyGenerationForCKDSigning(t, suite) + }) + + // Test signing with all nodes + t.Run("CKDSigningAllNodes", func(t *testing.T) { + testCKDSigningAllNodes(t, suite) + }) + + // // Test signing with one node offline + // t.Run("SigningOneNodeOffline", func(t *testing.T) { + // testSigningOneNodeOffline(t, suite) + // }) +} + +func testKeyGenerationForCKDSigning(t *testing.T, suite *E2ETestSuite) { + t.Log("Testing key generation for signing tests...") + + // Ensure MPC client is initialized + if suite.mpcClient == nil { + t.Fatal("MPC client is not initialized. Make sure Setup subtest runs first.") + } + + // Wait for all nodes to be ready before proceeding + suite.WaitForNodesReady(t) + + // Generate 1 wallet ID for testing + walletIDs := make([]string, 1) + for i := 0; i < 1; i++ { + walletIDs[i] = uuid.New().String() + suite.walletIDs = append(suite.walletIDs, walletIDs[i]) + } + + t.Logf("Generated wallet IDs: %v", walletIDs) + + // Setup result listener + err := suite.mpcClient.OnWalletCreationResult(func(result event.KeygenResultEvent) { + t.Logf("Received keygen result for wallet %s: %s", result.WalletID, result.ResultType) + suite.keygenResults[result.WalletID] = &result + + if result.ResultType == event.ResultTypeError { + t.Logf("Keygen failed for wallet %s: %s (%s)", result.WalletID, result.ErrorReason, result.ErrorCode) + } else { + t.Logf("Keygen succeeded for wallet %s", result.WalletID) + } + }) + require.NoError(t, err, "Failed to setup keygen result listener") + + // Add a small delay to ensure the result listener is fully set up + time.Sleep(10 * time.Second) + + // Trigger key generation for all wallets + for _, walletID := range walletIDs { + t.Logf("Triggering key generation for wallet %s", walletID) + + err := suite.mpcClient.CreateWallet(walletID) + require.NoError(t, err, "Failed to trigger key generation for wallet %s", walletID) + + // Small delay between requests to avoid overwhelming the system + time.Sleep(500 * time.Millisecond) + } + + // Wait for key generation to complete + t.Log("Waiting for key generation to complete...") + + // Wait up to keygenTimeout for all results + timeout := time.NewTimer(keygenTimeout) + defer timeout.Stop() + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeout.C: + t.Logf("Timeout waiting for keygen results. Received %d/%d results", len(suite.keygenResults), len(walletIDs)) + // Don't fail immediately, let's check what we got + goto checkResults + case <-ticker.C: + t.Logf("Still waiting... Received %d/%d keygen results", len(suite.keygenResults), len(walletIDs)) + + if len(suite.keygenResults) >= len(walletIDs) { + goto checkResults + } + } + } + +checkResults: + // Check that we got results for all wallets + for _, walletID := range walletIDs { + result, exists := suite.keygenResults[walletID] + if !exists { + t.Errorf("No keygen result received for wallet %s", walletID) + continue + } + + if result.ResultType == event.ResultTypeError { + t.Errorf("Keygen failed for wallet %s: %s (%s)", walletID, result.ErrorReason, result.ErrorCode) + } else { + t.Logf("Keygen succeeded for wallet %s", result.WalletID) + assert.NotEmpty(t, result.ECDSAPubKey, "ECDSA public key should not be empty for wallet %s", walletID) + assert.NotEmpty(t, result.EDDSAPubKey, "EdDSA public key should not be empty for wallet %s", walletID) + } + } + + t.Log("Key generation for signing tests completed") +} + +func testCKDSigningAllNodes(t *testing.T, suite *E2ETestSuite) { + t.Log("Testing signing with all nodes online...") + + if len(suite.walletIDs) == 0 { + t.Fatal("No wallets available for signing. Make sure key generation ran first.") + } + + // Setup a shared signing result listener for all signing tests + signingResults := make(map[string]*event.SigningResultEvent) + err := suite.mpcClient.OnSignResult(func(result event.SigningResultEvent) { + t.Logf("Received signing result for wallet %s (tx: %s): %s", result.WalletID, result.TxID, result.ResultType) + // Use TxID as key to avoid conflicts between different signing operations + signingResults[result.TxID] = &result + + if result.ResultType == event.ResultTypeError { + t.Logf("Signing failed for wallet %s (tx: %s): %s (%s)", result.WalletID, result.TxID, result.ErrorReason, result.ErrorCode) + } else { + t.Logf("Signing succeeded for wallet %s (tx: %s)", result.WalletID, result.TxID) + } + }) + require.NoError(t, err, "Failed to setup signing result listener") + + // Wait for listener setup + time.Sleep(2 * time.Second) + + // Test messages to sign + testMessages := []string{ + "Hello, MPC World!", + "Test message 2", + "Test message 3", + } + + for _, walletID := range suite.walletIDs { + t.Logf("Testing signing for wallet %s", walletID) + + for i, message := range testMessages { + t.Logf("Signing message %d: %s", i+1, message) + + // Test ECDSA signing + t.Run(fmt.Sprintf("ECDSA_%s_%d", walletID, i), func(t *testing.T) { + testCKDECDSASigningWithSharedListener(t, suite, walletID, message, signingResults) + }) + + // Test EdDSA signing + t.Run(fmt.Sprintf("EdDSA_%s_%d", walletID, i), func(t *testing.T) { + testCKDEdDSASigningWithSharedListener(t, suite, walletID, message, signingResults) + }) + } + } + + t.Log("Signing with all nodes completed") +} + +func testCKDECDSASigningWithSharedListener(t *testing.T, suite *E2ETestSuite, walletID, message string, signingResults map[string]*event.SigningResultEvent) { + t.Logf("Testing ECDSA signing for wallet %s with message: %s", walletID, message) + + // Wait for listener setup + time.Sleep(1 * time.Second) + + // Create a signing transaction message + txID := uuid.New().String() + signTxMsg := &types.SignTxMessage{ + WalletID: walletID, + TxID: txID, + Tx: []byte(message), + KeyType: types.KeyTypeSecp256k1, + NetworkInternalCode: "test", + DerivationPath: []uint32{44, 60, 0, 0, 0}, + } + + // Trigger ECDSA signing + err := suite.mpcClient.SignTransaction(signTxMsg) + require.NoError(t, err, "Failed to trigger ECDSA signing for wallet %s", walletID) + + // Wait for signing result + timeout := time.NewTimer(signingTimeout) + defer timeout.Stop() + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeout.C: + t.Fatalf("Timeout waiting for ECDSA signing result for wallet %s", walletID) + case <-ticker.C: + if result, exists := signingResults[txID]; exists { + logger.Info("Received ECDSA signing result for wallet", "result", result) + if result.ResultType == event.ResultTypeError { + t.Errorf("ECDSA signing failed for wallet %s: %s (%s)", walletID, result.ErrorReason, result.ErrorCode) + } else { + t.Logf("ECDSA signing succeeded for wallet %s", walletID) + assert.NotEmpty(t, result.R, "ECDSA R value should not be empty for wallet %s", walletID) + assert.NotEmpty(t, result.S, "ECDSA S value should not be empty for wallet %s", walletID) + assert.NotEmpty(t, result.SignatureRecovery, "ECDSA signature recovery should not be empty for wallet %s", walletID) + + // Compose the signature using the proper function + composedSig, err := ComposeSignature(result.SignatureRecovery, result.R, result.S) + if err != nil { + t.Errorf("Failed to compose ECDSA signature for wallet %s: %v", walletID, err) + } else { + t.Logf("Successfully composed ECDSA signature for wallet %s: %d bytes", walletID, len(composedSig)) + assert.Equal(t, 65, len(composedSig), "Composed ECDSA signature should be 65 bytes for wallet %s", walletID) + + // Log the signature components for debugging + t.Logf("ECDSA signature components - R: %d bytes, S: %d bytes, V: %d bytes", + len(result.R), len(result.S), len(result.SignatureRecovery)) + } + } + return + } + } + } +} + +func testCKDEdDSASigningWithSharedListener(t *testing.T, suite *E2ETestSuite, walletID, message string, signingResults map[string]*event.SigningResultEvent) { + t.Logf("Testing EdDSA signing for wallet %s with message: %s", walletID, message) + + // Wait for listener setup + time.Sleep(1 * time.Second) + + // Create a signing transaction message + txID := uuid.New().String() + signTxMsg := &types.SignTxMessage{ + WalletID: walletID, + TxID: txID, + Tx: []byte(message), + KeyType: types.KeyTypeEd25519, + NetworkInternalCode: "test", + DerivationPath: []uint32{44, 60, 0, 0, 1}, + } + + // Trigger EdDSA signing + err := suite.mpcClient.SignTransaction(signTxMsg) + require.NoError(t, err, "Failed to trigger EdDSA signing for wallet %s", walletID) + + // Wait for signing result + timeout := time.NewTimer(signingTimeout) + defer timeout.Stop() + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeout.C: + t.Fatalf("Timeout waiting for EdDSA signing result for wallet %s", walletID) + case <-ticker.C: + if result, exists := signingResults[txID]; exists { + logger.Info("Received EdDSA signing result for wallet", "result", result) + if result.ResultType == event.ResultTypeError { + t.Errorf("EdDSA signing failed for wallet %s: %s (%s)", walletID, result.ErrorReason, result.ErrorCode) + } else { + t.Logf("EdDSA signing succeeded for wallet %s", walletID) + assert.NotEmpty(t, result.Signature, "EdDSA signature should not be empty for wallet %s", walletID) + + // EdDSA signatures are typically 64 bytes (32 bytes R + 32 bytes S) + t.Logf("EdDSA signature length: %d bytes", len(result.Signature)) + if len(result.Signature) > 0 { + assert.Equal(t, 64, len(result.Signature), "EdDSA signature should be 64 bytes for wallet %s", walletID) + } + } + return + } + } + } +} diff --git a/examples/ckd/main.go b/examples/ckd/main.go new file mode 100644 index 0000000..29c193f --- /dev/null +++ b/examples/ckd/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "slices" + "syscall" + + "github.com/fystack/mpcium/pkg/client" + "github.com/fystack/mpcium/pkg/config" + "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/logger" + "github.com/fystack/mpcium/pkg/types" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/spf13/viper" +) + +func main() { + const environment = "dev" + config.InitViperConfig("") + logger.Init(environment, true) + + algorithm := viper.GetString("event_initiator_algorithm") + if algorithm == "" { + algorithm = string(types.EventInitiatorKeyTypeEd25519) + } + + // Validate algorithm + if !slices.Contains( + []string{ + string(types.EventInitiatorKeyTypeEd25519), + string(types.EventInitiatorKeyTypeP256), + }, + algorithm, + ) { + logger.Fatal( + fmt.Sprintf( + "invalid algorithm: %s. Must be %s or %s", + algorithm, + types.EventInitiatorKeyTypeEd25519, + types.EventInitiatorKeyTypeP256, + ), + nil, + ) + } + natsURL := viper.GetString("nats.url") + natsConn, err := nats.Connect(natsURL) + if err != nil { + logger.Fatal("Failed to connect to NATS", err) + } + defer natsConn.Drain() + defer natsConn.Close() + + localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{ + KeyPath: "./event_initiator.key", + }) + if err != nil { + logger.Fatal("Failed to create local signer", err) + } + + mpcClient := client.NewMPCClient(client.Options{ + NatsConn: natsConn, + Signer: localSigner, + }) + + // 2) Once wallet exists, immediately fire a SignTransaction + txID := uuid.New().String() + dummyTx := []byte("deadbeef") // replace with real transaction bytes + + txMsg := &types.SignTxMessage{ + KeyType: types.KeyTypeEd25519, + WalletID: "739c2f58-8385-4c40-a642-9a8a1e0d336f", + NetworkInternalCode: "solana-devnet", + TxID: txID, + Tx: dummyTx, + DerivationPath: []uint32{1, 2, 3}, + } + err = mpcClient.SignTransaction(txMsg) + if err != nil { + logger.Fatal("SignTransaction failed", err) + } + fmt.Printf("SignTransaction(%q) sent, awaiting result...\n", txID) + + // 3) Listen for signing results + err = mpcClient.OnSignResult(func(evt event.SigningResultEvent) { + logger.Info("Signing result received", + "txID", evt.TxID, + "signature", fmt.Sprintf("%x", evt.Signature), + ) + }) + if err != nil { + logger.Fatal("Failed to subscribe to OnSignResult", err) + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + <-stop + + fmt.Println("Shutting down.") +} diff --git a/examples/hdwallet/ecdsa/main.go b/examples/hdwallet/ecdsa/main.go new file mode 100644 index 0000000..f73774f --- /dev/null +++ b/examples/hdwallet/ecdsa/main.go @@ -0,0 +1,437 @@ +package main + +import ( + "crypto/ecdsa" + "encoding/hex" + "fmt" + "math/big" + "os" + "slices" + "strings" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/fystack/mpcium/pkg/ckdutil" + "github.com/fystack/mpcium/pkg/client" + "github.com/fystack/mpcium/pkg/config" + "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/logger" + "github.com/fystack/mpcium/pkg/types" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/spf13/viper" + "golang.org/x/crypto/sha3" +) + +const ( + // Ethereum derivation path: m/44/60/0/0/x + ethPurpose = 44 // BIP44 + ethCoinType = 60 // Ethereum + ethAccount = 0 // Account 0 + ethChange = 0 // External chain + + // Number of addresses to derive for the example run. + derivedAddressCount = uint32(2) +) + +type DerivedAddress struct { + Index uint32 + DerivationPath []uint32 + PublicKey []byte + Address string +} + +func main() { + fmt.Println("========================================") + fmt.Println(" MPC HD Wallet - Ethereum (ECDSA) Example") + fmt.Println("========================================") + fmt.Println() + + const environment = "dev" + config.InitViperConfig("") + logger.Init(environment, true) + + algorithm := viper.GetString("event_initiator_algorithm") + if algorithm == "" { + algorithm = string(types.EventInitiatorKeyTypeEd25519) + } + + if !slices.Contains( + []string{ + string(types.EventInitiatorKeyTypeEd25519), + string(types.EventInitiatorKeyTypeP256), + }, + algorithm, + ) { + logger.Fatal( + fmt.Sprintf( + "invalid algorithm: %s. Must be %s or %s", + algorithm, + types.EventInitiatorKeyTypeEd25519, + types.EventInitiatorKeyTypeP256, + ), + nil, + ) + } + + natsURL := viper.GetString("nats.url") + natsConn, err := nats.Connect(natsURL) + if err != nil { + logger.Fatal("Failed to connect to NATS", err) + } + defer natsConn.Drain() + defer natsConn.Close() + + localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{ + KeyPath: "./event_initiator.key", + }) + if err != nil { + logger.Fatal("Failed to create local signer", err) + } + + mpcClient := client.NewMPCClient(client.Options{ + NatsConn: natsConn, + Signer: localSigner, + }) + + // Step 1: Generate ONE master wallet + fmt.Println("Step 1: Generating master MPC wallet...") + fmt.Println() + + masterWalletID := uuid.New().String() + var masterPubKey []byte + var wg sync.WaitGroup + + // Listen for wallet creation result + wg.Add(1) + err = mpcClient.OnWalletCreationResult(func(evt event.KeygenResultEvent) { + if evt.WalletID == masterWalletID { + if evt.ResultType == event.ResultTypeError { + logger.Error("Master wallet creation failed", + fmt.Errorf("%s: %s", evt.ErrorCode, evt.ErrorReason), + "walletID", evt.WalletID, + ) + } else { + masterPubKey = evt.ECDSAPubKey // 64 bytes: X || Y + logger.Info("Master wallet created successfully", + "walletID", evt.WalletID, + "pubkey_length", len(masterPubKey), + ) + } + wg.Done() + } + }) + if err != nil { + logger.Fatal("Failed to subscribe to wallet creation results", err) + } + + // Create master wallet + if err := mpcClient.CreateWallet(masterWalletID); err != nil { + logger.Fatal("Failed to create master wallet", err) + } + + // Wait for master wallet creation + wg.Wait() + + if len(masterPubKey) == 0 { + fmt.Println("\n❌ Master wallet creation failed. Exiting.") + os.Exit(1) + } + + fmt.Println("\n✅ Master wallet created successfully!") + fmt.Printf(" Wallet ID: %s\n", masterWalletID) + fmt.Printf(" Public Key (64 bytes): %s...\n", hex.EncodeToString(masterPubKey)[:40]) + fmt.Println() + + // Step 2: Derive 2 addresses from master public key (client-side!) + fmt.Println("Step 2: Deriving addresses from master public key...") + fmt.Println(" (This is done CLIENT-SIDE, no MPC needed!)") + fmt.Println() + + chainCodeHex := viper.GetString("chain_code") + if chainCodeHex == "" { + logger.Fatal("chain_code not found in config", fmt.Errorf("required for HD derivation")) + } + + addresses := make([]*DerivedAddress, derivedAddressCount) + for i := uint32(0); i < derivedAddressCount; i++ { + childIndex := i + path := []uint32{ethPurpose, ethCoinType, ethAccount, ethChange, childIndex} + + // Derive child public key from master (NO MPC!) + childPubKey, err := deriveChildPublicKey(masterPubKey, chainCodeHex, path) + if err != nil { + logger.Fatal("Failed to derive child public key", err) + } + + address := deriveEthereumAddress(childPubKey) + + addresses[i] = &DerivedAddress{ + Index: childIndex, + DerivationPath: path, + PublicKey: childPubKey, + Address: address, + } + } + + // Display derived addresses + fmt.Println("========================================") + fmt.Println(" Derived Addresses (from Master)") + fmt.Println("========================================") + fmt.Println() + + for _, addr := range addresses { + fmt.Printf("Address %d:\n", addr.Index+1) + fmt.Printf(" Derivation Path: m/%d/%d/%d/%d/%d\n", + addr.DerivationPath[0], addr.DerivationPath[1], addr.DerivationPath[2], + addr.DerivationPath[3], addr.DerivationPath[4]) + fmt.Printf(" Public Key: %s...\n", hex.EncodeToString(addr.PublicKey)[:40]) + fmt.Printf(" Ethereum Address: %s\n", addr.Address) + fmt.Println() + } + + // Step 3: Sequential signing & verification + fmt.Println("========================================") + fmt.Println(" Sequential Signing & Verification") + fmt.Println("========================================") + fmt.Println() + fmt.Println("Signing each derived address sequentially and verifying locally.") + fmt.Println() + + var mu sync.Mutex + resultChans := make(map[string]chan event.SigningResultEvent) + + err = mpcClient.OnSignResult(func(evt event.SigningResultEvent) { + mu.Lock() + ch, ok := resultChans[evt.TxID] + mu.Unlock() + + if ok { + ch <- evt + } + }) + if err != nil { + logger.Fatal("Failed to subscribe to signing results", err) + } + + successCount := 0 + verifiedCount := 0 + + for _, addr := range addresses { + txMsg := fmt.Sprintf("Sequential signing from address %d (%s)", addr.Index+1, addr.Address) + + // Hash the message to 32 bytes (required for ECDSA signing) + hash := sha3.NewLegacyKeccak256() + hash.Write([]byte(txMsg)) + txHash := hash.Sum(nil) + + txID := uuid.New().String() + + resultCh := make(chan event.SigningResultEvent, 1) + + mu.Lock() + resultChans[txID] = resultCh + mu.Unlock() + + logger.Info("Derivaition path", "path", addr.DerivationPath) + + signTxMsg := &types.SignTxMessage{ + WalletID: masterWalletID, + TxID: txID, + Tx: txHash, + KeyType: types.KeyTypeSecp256k1, + NetworkInternalCode: "ethereum-mainnet", + DerivationPath: addr.DerivationPath, + } + + fmt.Printf("📝 Address %d: Signing with path m/%d/%d/%d/%d/%d...\n", + addr.Index+1, + addr.DerivationPath[0], addr.DerivationPath[1], addr.DerivationPath[2], + addr.DerivationPath[3], addr.DerivationPath[4]) + + if err := mpcClient.SignTransaction(signTxMsg); err != nil { + logger.Error("Failed to initiate signing", err) + mu.Lock() + delete(resultChans, txID) + mu.Unlock() + close(resultCh) + continue + } + + var result event.SigningResultEvent + select { + case result = <-resultCh: + mu.Lock() + delete(resultChans, txID) + mu.Unlock() + close(resultCh) + case <-time.After(45 * time.Second): + fmt.Printf("❌ Address %d: Timed out waiting for signing result\n", addr.Index+1) + mu.Lock() + delete(resultChans, txID) + mu.Unlock() + close(resultCh) + continue + } + + if result.ResultType == event.ResultTypeError { + fmt.Printf("❌ Address %d: Signing failed - %s (%s)\n", + addr.Index+1, result.ErrorReason, result.ErrorCode) + continue + } + + successCount++ + + fmt.Printf("✅ Address %d: Signed successfully\n", addr.Index+1) + fmt.Printf(" R: %s\n", hex.EncodeToString(result.R)) + fmt.Printf(" S: %s\n", hex.EncodeToString(result.S)) + fmt.Printf(" V: %s\n", hex.EncodeToString(result.SignatureRecovery)) + + valid, err := verifySignature(txHash, addr.PublicKey, result.R, result.S) + if err != nil { + fmt.Printf(" ⚠️ Unable to verify signature: %v\n", err) + continue + } + + if valid { + verifiedCount++ + fmt.Println(" 🔐 Signature verified against derived public key.") + } else { + fmt.Println(" ⚠️ Signature verification failed.") + } + } + + // Summary + fmt.Println() + fmt.Println("========================================") + fmt.Println(" Summary") + fmt.Println("========================================") + fmt.Println() + fmt.Printf("Master Wallet ID: %s\n", masterWalletID) + fmt.Printf("Addresses derived: 2\n") + fmt.Printf("Signatures success: %d\n", successCount) + fmt.Printf("Signatures failed: %d\n", len(addresses)-successCount) + fmt.Printf("Verified locally: %d\n", verifiedCount) + fmt.Println() + + if successCount == 2 { + fmt.Println("✅ All transactions signed successfully!") + fmt.Println() + fmt.Println("📚 What happened:") + fmt.Println(" 1. Created ONE master MPC wallet") + fmt.Println(" 2. Derived 2 addresses CLIENT-SIDE (no MPC)") + fmt.Println(" 3. MPC derived child keys during signing") + fmt.Println(" 4. Verified signatures locally against derived keys") + } + + fmt.Println("\nDone!") +} + +// deriveChildPublicKey derives child key CLIENT-SIDE (no MPC) using ckdutil. +func deriveChildPublicKey(masterPubKey []byte, chainCodeHex string, path []uint32) ([]byte, error) { + if len(masterPubKey) != 64 { + return nil, fmt.Errorf("invalid master key length: %d", len(masterPubKey)) + } + + uncompressed := append([]byte{0x04}, masterPubKey...) + masterPub, err := btcec.ParsePubKey(uncompressed) + if err != nil { + return nil, fmt.Errorf("parse master pubkey: %w", err) + } + + childCompressed, err := ckdutil.DeriveSecp256k1ChildCompressed( + masterPub.SerializeCompressed(), + chainCodeHex, + path, + ) + if err != nil { + return nil, fmt.Errorf("derive child pubkey: %w", err) + } + + childPub, err := btcec.ParsePubKey(childCompressed) + if err != nil { + return nil, fmt.Errorf("parse child pubkey: %w", err) + } + + return serializeUncompressed(childPub), nil +} + +func deriveEthereumAddress(pubKey []byte) string { + if len(pubKey) != 64 { + logger.Error("Invalid pubkey length", fmt.Errorf("got %d", len(pubKey))) + return "" + } + + hash := sha3.NewLegacyKeccak256() + hash.Write(pubKey) + hashBytes := hash.Sum(nil) + + addressBytes := hashBytes[len(hashBytes)-20:] + address := "0x" + hex.EncodeToString(addressBytes) + + return toChecksumAddress(address) +} + +func toChecksumAddress(address string) string { + address = strings.ToLower(strings.TrimPrefix(address, "0x")) + + hash := sha3.NewLegacyKeccak256() + hash.Write([]byte(address)) + hashBytes := hash.Sum(nil) + + result := "0x" + for i := 0; i < len(address); i++ { + hashByte := hashBytes[i/2] + if i%2 == 0 { + hashByte = hashByte >> 4 + } else { + hashByte = hashByte & 0xf + } + + if hashByte >= 8 { + result += strings.ToUpper(string(address[i])) + } else { + result += string(address[i]) + } + } + + return result +} + +func verifySignature(message, pubKey, rBytes, sBytes []byte) (bool, error) { + if len(pubKey) != 64 { + return false, fmt.Errorf("invalid public key length: %d", len(pubKey)) + } + + if len(rBytes) == 0 || len(sBytes) == 0 { + return false, fmt.Errorf("signature components missing") + } + + curve := btcec.S256() + x := new(big.Int).SetBytes(pubKey[:32]) + y := new(big.Int).SetBytes(pubKey[32:]) + + if !curve.IsOnCurve(x, y) { + return false, fmt.Errorf("public key not on secp256k1 curve") + } + + r := new(big.Int).SetBytes(rBytes) + s := new(big.Int).SetBytes(sBytes) + + if r.Sign() <= 0 || s.Sign() <= 0 { + return false, fmt.Errorf("invalid signature values") + } + + ok := ecdsa.Verify(&ecdsa.PublicKey{Curve: curve, X: x, Y: y}, message, r, s) + return ok, nil +} + +func serializeUncompressed(pub *btcec.PublicKey) []byte { + out := make([]byte, 64) + xBytes := pub.X().Bytes() + yBytes := pub.Y().Bytes() + copy(out[32-len(xBytes):32], xBytes) + copy(out[64-len(yBytes):], yBytes) + return out +} diff --git a/examples/hdwallet/eddsa/main.go b/examples/hdwallet/eddsa/main.go new file mode 100644 index 0000000..2654cf7 --- /dev/null +++ b/examples/hdwallet/eddsa/main.go @@ -0,0 +1,416 @@ +package main + +import ( + "encoding/hex" + "fmt" + "os" + "os/signal" + "slices" + "sync" + "syscall" + "time" + + tsscrypto "github.com/bnb-chain/tss-lib/v2/crypto" + "github.com/bnb-chain/tss-lib/v2/tss" + "github.com/btcsuite/btcutil/base58" + "github.com/decred/dcrd/dcrec/edwards/v2" + "github.com/fystack/mpcium/pkg/ckdutil" + "github.com/fystack/mpcium/pkg/client" + "github.com/fystack/mpcium/pkg/config" + "github.com/fystack/mpcium/pkg/encoding" + "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/logger" + "github.com/fystack/mpcium/pkg/mpc" + "github.com/fystack/mpcium/pkg/types" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/spf13/viper" + "golang.org/x/crypto/sha3" +) + +const ( + // Solana derivation path: m/44'/501'/x'/0' + solPurpose = 44 // BIP44 + solCoinType = 501 // Solana + solChange = 0 // External chain + + // Number of addresses to derive; change this to derive more or fewer addresses. + addressCount = uint32(2) +) + +type DerivedAddress struct { + Index uint32 + DerivationPath []uint32 + PublicKey []byte + Address string +} + +func main() { + fmt.Println("========================================") + fmt.Println(" MPC HD Wallet - Solana (EdDSA) Example") + fmt.Println("========================================") + fmt.Println() + + const environment = "dev" + config.InitViperConfig("") + logger.Init(environment, true) + + algorithm := viper.GetString("event_initiator_algorithm") + if algorithm == "" { + algorithm = string(types.EventInitiatorKeyTypeEd25519) + } + + if !slices.Contains( + []string{ + string(types.EventInitiatorKeyTypeEd25519), + string(types.EventInitiatorKeyTypeP256), + }, + algorithm, + ) { + logger.Fatal( + fmt.Sprintf( + "invalid algorithm: %s. Must be %s or %s", + algorithm, + types.EventInitiatorKeyTypeEd25519, + types.EventInitiatorKeyTypeP256, + ), + nil, + ) + } + + natsURL := viper.GetString("nats.url") + natsConn, err := nats.Connect(natsURL) + if err != nil { + logger.Fatal("Failed to connect to NATS", err) + } + defer natsConn.Drain() + defer natsConn.Close() + + localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{ + KeyPath: "./event_initiator.key", + }) + if err != nil { + logger.Fatal("Failed to create local signer", err) + } + + mpcClient := client.NewMPCClient(client.Options{ + NatsConn: natsConn, + Signer: localSigner, + }) + + // Step 1: Generate ONE master wallet + fmt.Println("Step 1: Generating master MPC wallet...") + fmt.Println() + + masterWalletID := uuid.New().String() + var masterPubKey []byte + var wg sync.WaitGroup + + // Listen for wallet creation result + wg.Add(1) + err = mpcClient.OnWalletCreationResult(func(evt event.KeygenResultEvent) { + if evt.WalletID == masterWalletID { + if evt.ResultType == event.ResultTypeError { + logger.Error("Master wallet creation failed", + fmt.Errorf("%s: %s", evt.ErrorCode, evt.ErrorReason), + "walletID", evt.WalletID, + ) + } else { + masterPubKey = evt.EDDSAPubKey // 32 bytes for Ed25519 + logger.Info("Master wallet created successfully", + "walletID", evt.WalletID, + "pubkey_length", len(masterPubKey), + ) + } + wg.Done() + } + }) + if err != nil { + logger.Fatal("Failed to subscribe to wallet creation results", err) + } + + // Create master wallet + if err := mpcClient.CreateWallet(masterWalletID); err != nil { + logger.Fatal("Failed to create master wallet", err) + } + + // Wait for master wallet creation + wg.Wait() + + if len(masterPubKey) == 0 { + fmt.Println("\n❌ Master wallet creation failed. Exiting.") + os.Exit(1) + } + + fmt.Println("\n✅ Master wallet created successfully!") + fmt.Printf(" Wallet ID: %s\n", masterWalletID) + fmt.Printf(" Public Key (32 bytes): %s...\n", hex.EncodeToString(masterPubKey)[:40]) + fmt.Println() + + // Step 2: Derive addresses from master public key (client-side!) + fmt.Println("Step 2: Deriving Solana addresses from master public key...") + fmt.Println(" (This is done CLIENT-SIDE, no MPC needed!)") + fmt.Println() + + chainCodeHex := viper.GetString("chain_code") + if chainCodeHex == "" { + logger.Fatal("chain_code not found in config", fmt.Errorf("required for HD derivation")) + } + + addresses := make([]*DerivedAddress, addressCount) + for i := uint32(0); i < addressCount; i++ { + path := []uint32{solPurpose, solCoinType, i, solChange} + + // Derive child public key from master (NO MPC!) + childPubKey, err := deriveChildPublicKeyEd25519(masterPubKey, chainCodeHex, path) + if err != nil { + logger.Fatal("Failed to derive child public key", err) + } + + // Optional sanity check: compare with tss-lib CKD to ensure parity. + if tssChild, err := deriveChildPublicKeyEd25519ViaTSS(masterPubKey, chainCodeHex, path, masterWalletID); err != nil { + logger.Warn("Unable to compare with tss-lib CKD", "error", err) + } else if !slices.Equal(childPubKey, tssChild) { + logger.Warn("Derived child pubkey mismatch between local CKD and tss-lib", "path", path) + } else { + logger.Info("Derived child pubkey matches tss-lib", "path", path) + } + + address := deriveSolanaAddress(childPubKey) + + addresses[i] = &DerivedAddress{ + Index: i, + DerivationPath: path, + PublicKey: childPubKey, + Address: address, + } + } + + // Display derived addresses + fmt.Println("========================================") + fmt.Println(" Derived Addresses (from Master)") + fmt.Println("========================================") + fmt.Println() + + for _, addr := range addresses { + fmt.Printf("Address %d:\n", addr.Index+1) + fmt.Printf(" Derivation Path: m/%d/%d/%d/%d\n", + addr.DerivationPath[0], addr.DerivationPath[1], + addr.DerivationPath[2], addr.DerivationPath[3]) + fmt.Printf(" Public Key: %s...\n", hex.EncodeToString(addr.PublicKey)[:40]) + fmt.Printf(" Solana Address: %s\n", addr.Address) + fmt.Println() + } + + // Step 3: Sequential signing & verification + fmt.Println("========================================") + fmt.Println(" Sequential Signing & Verification") + fmt.Println("========================================") + fmt.Println() + fmt.Println("Signing each derived address sequentially and verifying locally.") + fmt.Println() + + var mu sync.Mutex + resultChans := make(map[string]chan event.SigningResultEvent) + + err = mpcClient.OnSignResult(func(evt event.SigningResultEvent) { + mu.Lock() + ch, ok := resultChans[evt.TxID] + mu.Unlock() + + if ok { + ch <- evt + } + }) + if err != nil { + logger.Fatal("Failed to subscribe to signing results", err) + } + + successCount := 0 + verifiedCount := 0 + + for _, addr := range addresses { + txMsg := fmt.Sprintf("Sequential signing from address %d (%s)", addr.Index+1, addr.Address) + + // Hash the message to 32 bytes (required for EdDSA signing) + hash := sha3.NewLegacyKeccak256() + hash.Write([]byte(txMsg)) + txHash := hash.Sum(nil) + + txID := uuid.New().String() + + resultCh := make(chan event.SigningResultEvent, 1) + + mu.Lock() + resultChans[txID] = resultCh + mu.Unlock() + + logger.Info("Derivation path", "path", addr.DerivationPath) + + signTxMsg := &types.SignTxMessage{ + WalletID: masterWalletID, + TxID: txID, + Tx: txHash, + KeyType: types.KeyTypeEd25519, + NetworkInternalCode: "solana-devnet", + DerivationPath: addr.DerivationPath, + } + + fmt.Printf("📝 Address %d: Signing with path m/%d/%d/%d/%d...\n", + addr.Index+1, + addr.DerivationPath[0], addr.DerivationPath[1], + addr.DerivationPath[2], addr.DerivationPath[3]) + + if err := mpcClient.SignTransaction(signTxMsg); err != nil { + logger.Error("Failed to initiate signing", err) + mu.Lock() + delete(resultChans, txID) + mu.Unlock() + close(resultCh) + continue + } + + var result event.SigningResultEvent + select { + case result = <-resultCh: + mu.Lock() + delete(resultChans, txID) + mu.Unlock() + close(resultCh) + case <-time.After(45 * time.Second): + fmt.Printf("❌ Address %d: Timed out waiting for signing result\n", addr.Index+1) + mu.Lock() + delete(resultChans, txID) + mu.Unlock() + close(resultCh) + continue + } + + if result.ResultType == event.ResultTypeError { + fmt.Printf("❌ Address %d: Signing failed - %s (%s)\n", + addr.Index+1, result.ErrorReason, result.ErrorCode) + continue + } + + successCount++ + + fmt.Printf("✅ Address %d: Signed successfully\n", addr.Index+1) + fmt.Printf(" Signature: %s\n", hex.EncodeToString(result.Signature)) + + valid := verifySignatureEd25519(txHash, addr.PublicKey, result.Signature) + if valid { + verifiedCount++ + fmt.Println(" 🔐 Signature verified against derived public key.") + } else { + fmt.Println(" ⚠️ Signature verification failed.") + } + } + + // Summary + fmt.Println() + fmt.Println("========================================") + fmt.Println(" Summary") + fmt.Println("========================================") + fmt.Println() + fmt.Printf("Master Wallet ID: %s\n", masterWalletID) + fmt.Printf("Addresses derived: %d\n", len(addresses)) + fmt.Printf("Signatures success: %d\n", successCount) + fmt.Printf("Signatures failed: %d\n", len(addresses)-successCount) + fmt.Printf("Verified locally: %d\n", verifiedCount) + fmt.Println() + + if successCount == len(addresses) { + fmt.Println("✅ All transactions signed successfully!") + fmt.Println() + fmt.Println("📚 What happened:") + fmt.Println(" 1. Created ONE master MPC wallet") + fmt.Printf(" 2. Derived %d Solana addresses CLIENT-SIDE (no MPC)\n", len(addresses)) + fmt.Println(" 3. MPC derived child keys during signing") + fmt.Println(" 4. Verified signatures locally against derived keys") + } + + fmt.Println("\nDone!") + + // Keep running + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + <-stop +} + +// deriveChildPublicKeyEd25519 derives Ed25519 child key CLIENT-SIDE (no MPC) +func deriveChildPublicKeyEd25519(masterPubKey []byte, chainCodeHex string, path []uint32) ([]byte, error) { + if len(masterPubKey) == 0 { + return nil, fmt.Errorf("master public key is empty") + } + + derivedBytes, err := ckdutil.DeriveEd25519ChildCompressed(masterPubKey, chainCodeHex, path) + if err != nil { + return nil, err + } + + if len(derivedBytes) != 32 { + return nil, fmt.Errorf("unexpected derived pubkey length: %d", len(derivedBytes)) + } + + return derivedBytes, nil +} + +func deriveSolanaAddress(pubKey []byte) string { + if len(pubKey) != 32 { + logger.Error("Invalid pubkey length for Solana", fmt.Errorf("got %d", len(pubKey))) + return "" + } + + // Solana address is just base58-encoded public key + address := base58.Encode(pubKey) + return address +} + +func verifySignatureEd25519(message, pubKey, signature []byte) bool { + if len(pubKey) == 0 || len(signature) == 0 { + return false + } + + decodedPub, err := encoding.DecodeEDDSAPubKey(pubKey) + if err != nil { + return false + } + + parsedSig, err := edwards.ParseSignature(signature) + if err != nil { + return false + } + + return edwards.Verify(decodedPub, message, parsedSig.R, parsedSig.S) +} + +// deriveChildPublicKeyEd25519ViaTSS mirrors the MPC node CKD path to validate parity. +func deriveChildPublicKeyEd25519ViaTSS(masterPubKey []byte, chainCodeHex string, path []uint32, walletID string) ([]byte, error) { + pubKey, err := encoding.DecodeEDDSAPubKey(masterPubKey) + if err != nil { + return nil, fmt.Errorf("decode master pubkey: %w", err) + } + + masterPoint, err := tsscrypto.NewECPoint(tss.Edwards(), pubKey.X, pubKey.Y) + if err != nil { + return nil, fmt.Errorf("build EC point from master pubkey: %w", err) + } + + ckd, err := mpc.NewCKDFromHex(chainCodeHex) + if err != nil { + return nil, fmt.Errorf("init CKD: %w", err) + } + + _, childKey, err := ckd.Derive(walletID, masterPoint, path, tss.Edwards()) + if err != nil { + return nil, fmt.Errorf("derive child key for path %v: %w", path, err) + } + + childPub := edwards.PublicKey{ + Curve: tss.Edwards(), + X: childKey.PublicKey.X(), + Y: childKey.PublicKey.Y(), + } + + return childPub.SerializeCompressed(), nil +} diff --git a/go.mod b/go.mod index d9cc99b..cf8dc21 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/fystack/mpcium -go 1.23.8 - -toolchain go1.24.7 +go 1.25.0 require ( filippo.io/age v1.2.1 @@ -11,6 +9,9 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.18.8 github.com/aws/aws-sdk-go-v2/service/kms v1.45.0 github.com/bnb-chain/tss-lib/v2 v2.0.2 + github.com/btcsuite/btcd v0.24.2 + github.com/btcsuite/btcd/btcec/v2 v2.3.2 + github.com/btcsuite/btcutil v1.0.2 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 github.com/dgraph-io/badger/v4 v4.7.0 github.com/google/uuid v1.6.0 @@ -40,10 +41,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.38.1 // indirect github.com/aws/smithy-go v1.23.0 // indirect - github.com/btcsuite/btcd v0.24.2 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/btcsuite/btcutil v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect @@ -102,8 +100,9 @@ require ( golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/agl/ed25519 => github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43 + +replace github.com/bnb-chain/tss-lib/v2 => github.com/fystack/tss-lib/v2 v2.0.3 diff --git a/go.sum b/go.sum index da2768e..697a3ea 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43 h1:Vkf7rtHx8uHx8gDfkQaCdVfc+gfrF9v6sR6xJy7RXNg= github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43/go.mod h1:TnVqVdGEK8b6erOMkcyYGWzCQMw7HEMCOw3BgFYCFWs= -github.com/bnb-chain/tss-lib/v2 v2.0.2 h1:dL2GJFCSYsYQ0bHkGll+hNM2JWsC1rxDmJJJQEmUy9g= -github.com/bnb-chain/tss-lib/v2 v2.0.2/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= @@ -118,6 +116,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fystack/tss-lib/v2 v2.0.3 h1:A0HGL5GDPpKbNW+0ZXgv1Ri3+ks88AvxTS7OK40gnUY= +github.com/fystack/tss-lib/v2 v2.0.3/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -158,13 +158,10 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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/hashicorp/consul/api v1.26.1 h1:5oSXOO5fboPZeW5SN+TdGFP/BILDgBm19OrPZ/pICIM= -github.com/hashicorp/consul/api v1.26.1/go.mod h1:B4sQTeaSO16NtynqrAdwOlahJ7IUDZM9cj2420xYL8A= github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= -github.com/hashicorp/consul/sdk v0.15.0 h1:2qK9nDrr4tiJKRoxPGhm6B7xJjLVIQqkjiab2M4aKjU= -github.com/hashicorp/consul/sdk v0.15.0/go.mod h1:r/OmRRPbHOe0yxNahLw7G9x5WG17E1BIECMtCjcPSNo= github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= +github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -407,8 +404,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -545,9 +540,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/ckdutil/child_derivation.go b/pkg/ckdutil/child_derivation.go new file mode 100644 index 0000000..682dd3f --- /dev/null +++ b/pkg/ckdutil/child_derivation.go @@ -0,0 +1,183 @@ +package ckdutil + +import ( + "crypto/hmac" + "crypto/sha512" + "encoding/binary" + "encoding/hex" + "fmt" + "math/big" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/decred/dcrd/dcrec/edwards/v2" +) + +const ( + hardenedKeyStart = 0x80000000 + // Compressed pubkey: 1-byte prefix (02/03) + 32-byte X coordinate. + pubKeyBytesLenCompressed = 33 + // BIP32 specifies child index serialized as 4-byte big-endian (ser32). + childIndexBytes = 4 + pubKeyCompressedEven byte = 0x2 + pubKeyCompressedOdd byte = 0x3 +) + +// DeriveEd25519ChildCompressed derives a non-hardened child public key on ed25519 and returns the 32-byte compressed key. +func DeriveEd25519ChildCompressed(masterPubKey []byte, chainCodeHex string, path []uint32) ([]byte, error) { + if len(masterPubKey) == 0 { + return nil, fmt.Errorf("master public key is empty") + } + + pubKey, err := edwards.ParsePubKey(masterPubKey) + if err != nil { + return nil, fmt.Errorf("decode master pubkey: %w", err) + } + + return deriveEd25519ChildCompressed(pubKey, chainCodeHex, path) +} + +// DeriveSecp256k1ChildCompressed derives a non-hardened child public key on secp256k1 and returns the 33-byte compressed key. +func DeriveSecp256k1ChildCompressed(masterPubKey []byte, chainCodeHex string, path []uint32) ([]byte, error) { + if len(masterPubKey) != 33 { + return nil, fmt.Errorf("invalid master pubkey length: %d", len(masterPubKey)) + } + + curve := btcec.S256() + pubKey, err := btcec.ParsePubKey(masterPubKey) + if err != nil { + return nil, fmt.Errorf("decode master pubkey: %w", err) + } + + chainCode, err := hex.DecodeString(chainCodeHex) + if err != nil { + return nil, fmt.Errorf("decode chain code: %w", err) + } + if len(chainCode) != 32 { + return nil, fmt.Errorf("invalid chain code length: %d", len(chainCode)) + } + + currentX := new(big.Int).Set(pubKey.X()) + currentY := new(big.Int).Set(pubKey.Y()) + currentChainCode := append([]byte(nil), chainCode...) + + for _, index := range path { + if index >= hardenedKeyStart { + return nil, fmt.Errorf("hardened derivation not supported: %d", index) + } + + data := make([]byte, pubKeyBytesLenCompressed+childIndexBytes) + copy(data, serializeCompressed(currentX, currentY)) + binary.BigEndian.PutUint32(data[pubKeyBytesLenCompressed:], index) + + mac := hmac.New(sha512.New, currentChainCode) + mac.Write(data) + ilr := mac.Sum(nil) + il := ilr[:32] + ir := ilr[32:] + + ilNum := new(big.Int).SetBytes(il) + if ilNum.Sign() == 0 || ilNum.Cmp(curve.Params().N) >= 0 { + return nil, fmt.Errorf("invalid IL for index %d", index) + } + + deltaX, deltaY := curve.ScalarBaseMult(ilNum.Bytes()) + childX, childY := curve.Add(currentX, currentY, deltaX, deltaY) + if childX == nil || childY == nil || childX.Sign() == 0 || childY.Sign() == 0 { + return nil, fmt.Errorf("invalid child point at index %d", index) + } + + currentX, currentY = childX, childY + currentChainCode = ir + } + + return serializeCompressed(currentX, currentY), nil +} + +// --- shared helpers (non-hardened) --- + +func deriveEd25519ChildCompressed(masterPub *edwards.PublicKey, chainCodeHex string, path []uint32) ([]byte, error) { + if masterPub == nil || masterPub.X == nil || masterPub.Y == nil { + return nil, fmt.Errorf("invalid master public key") + } + + chainCode, err := hex.DecodeString(chainCodeHex) + if err != nil { + return nil, fmt.Errorf("decode chain code: %w", err) + } + if len(chainCode) != 32 { + return nil, fmt.Errorf("invalid chain code length: %d", len(chainCode)) + } + + curve := edwards.Edwards() + currentX := new(big.Int).Set(masterPub.X) + currentY := new(big.Int).Set(masterPub.Y) + currentChainCode := append([]byte(nil), chainCode...) + + for _, index := range path { + if index >= hardenedKeyStart { + return nil, fmt.Errorf("hardened derivation not supported: %d", index) + } + + data := make([]byte, pubKeyBytesLenCompressed+childIndexBytes) + copy(data, serializeCompressed(currentX, currentY)) + binary.BigEndian.PutUint32(data[pubKeyBytesLenCompressed:], index) + + mac := hmac.New(sha512.New, currentChainCode) + mac.Write(data) + ilr := mac.Sum(nil) + il := ilr[:32] + ir := ilr[32:] + + ilNum := new(big.Int).SetBytes(il) + ilNum.Mod(ilNum, curve.Params().N) + if ilNum.Sign() == 0 || ilNum.Cmp(curve.Params().N) >= 0 { + return nil, fmt.Errorf("invalid IL for index %d", index) + } + + deltaX, deltaY := curve.ScalarBaseMult(ilNum.Bytes()) + childX, childY := curve.Add(currentX, currentY, deltaX, deltaY) + if childX == nil || childY == nil || childX.Sign() == 0 || childY.Sign() == 0 { + return nil, fmt.Errorf("invalid child point at index %d", index) + } + + currentX, currentY = childX, childY + currentChainCode = ir + } + + childPub := edwards.PublicKey{ + Curve: curve, + X: currentX, + Y: currentY, + } + + return childPub.SerializeCompressed(), nil +} + +// serializeCompressed matches the node compression (33 bytes). +func serializeCompressed(x, y *big.Int) []byte { + b := make([]byte, 0, pubKeyBytesLenCompressed) + format := pubKeyCompressedEven + if isOdd(y) { + format = pubKeyCompressedOdd + } + b = append(b, format) + return paddedAppend(b, 32, x.Bytes()) +} + +func isOdd(a *big.Int) bool { + return a.Bit(0) == 1 +} + +func paddedAppend(dst []byte, srcPaddedSize int, src []byte) []byte { + return append(dst, paddedBytes(srcPaddedSize, src)...) +} + +func paddedBytes(size int, src []byte) []byte { + offset := size - len(src) + tmp := src + if offset > 0 { + tmp = make([]byte, size) + copy(tmp[offset:], src) + } + return tmp +} diff --git a/pkg/ckdutil/child_derivation_test.go b/pkg/ckdutil/child_derivation_test.go new file mode 100644 index 0000000..b126822 --- /dev/null +++ b/pkg/ckdutil/child_derivation_test.go @@ -0,0 +1,79 @@ +package ckdutil + +import ( + "encoding/hex" + "testing" + + tsscrypto "github.com/bnb-chain/tss-lib/v2/crypto" + "github.com/bnb-chain/tss-lib/v2/tss" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/decred/dcrd/dcrec/edwards/v2" + "github.com/fystack/mpcium/pkg/mpc" + "github.com/stretchr/testify/require" +) + +func TestEd25519StandaloneMatchesTSS(t *testing.T) { + chainCode := make([]byte, 32) + for i := range chainCode { + chainCode[i] = byte(i + 1) + } + chainCodeHex := hex.EncodeToString(chainCode) + + curve := edwards.Edwards() + masterPub := edwards.PublicKey{ + Curve: curve, + X: curve.Params().Gx, + Y: curve.Params().Gy, + } + masterPubBytes := masterPub.SerializeCompressed() + + masterPoint, err := tsscrypto.NewECPoint(curve, masterPub.X, masterPub.Y) + require.NoError(t, err) + + ckd, err := mpc.NewCKDFromHex(chainCodeHex) + require.NoError(t, err) + + for i := 0; i < 100; i++ { + path := []uint32{44, 501, uint32(i), 0} + + localChild, err := DeriveEd25519ChildCompressed(masterPubBytes, chainCodeHex, path) + require.NoErrorf(t, err, "local derivation failed at index %d", i) + + _, tssChild, err := ckd.Derive("wallet-ed25519-test", masterPoint, path, tss.Edwards()) + require.NoErrorf(t, err, "tss derivation failed at index %d", i) + + tssPub := edwards.PublicKey{Curve: curve, X: tssChild.PublicKey.X(), Y: tssChild.PublicKey.Y()} + require.Equalf(t, tssPub.SerializeCompressed(), localChild, "pubkey mismatch at index %d", i) + } +} + +func TestSecp256k1StandaloneMatchesTSS(t *testing.T) { + chainCode := make([]byte, 32) + for i := range chainCode { + chainCode[i] = byte(0xaa - i) + } + chainCodeHex := hex.EncodeToString(chainCode) + + curve := btcec.S256() + masterX, masterY := curve.Params().Gx, curve.Params().Gy + masterPubBytes := serializeCompressed(masterX, masterY) + + masterPoint, err := tsscrypto.NewECPoint(curve, masterX, masterY) + require.NoError(t, err) + + ckd, err := mpc.NewCKDFromHex(chainCodeHex) + require.NoError(t, err) + + for i := 0; i < 1000; i++ { + path := []uint32{44, 60, 0, 0, uint32(i)} + + localChild, err := DeriveSecp256k1ChildCompressed(masterPubBytes, chainCodeHex, path) + require.NoErrorf(t, err, "local derivation failed at index %d", i) + + _, tssChild, err := ckd.Derive("wallet-secp-test", masterPoint, path, tss.S256()) + require.NoErrorf(t, err, "tss derivation failed at index %d", i) + + tssChildBytes := serializeCompressed(tssChild.PublicKey.X(), tssChild.PublicKey.Y()) + require.Equalf(t, tssChildBytes, localChild, "pubkey mismatch at index %d", i) + } +} diff --git a/pkg/config/init.go b/pkg/config/init.go index db8e733..2efa7ae 100644 --- a/pkg/config/init.go +++ b/pkg/config/init.go @@ -17,6 +17,7 @@ type AppConfig struct { Environment string `mapstructure:"environment"` BadgerPassword string `mapstructure:"badger_password"` + ChainCodeHex string `mapstructure:"chain_code"` } // Implement masking serializer AppConfig diff --git a/pkg/eventconsumer/event_consumer.go b/pkg/eventconsumer/event_consumer.go index af21881..8dd9e6a 100644 --- a/pkg/eventconsumer/event_consumer.go +++ b/pkg/eventconsumer/event_consumer.go @@ -402,6 +402,7 @@ func (ec *eventConsumer) handleSigningEvent(natMsg *nats.Msg) { msg.TxID, msg.NetworkInternalCode, ec.signingResultQueue, + msg.DerivationPath, idempotentKey, ) case types.KeyTypeEd25519: @@ -411,6 +412,7 @@ func (ec *eventConsumer) handleSigningEvent(natMsg *nats.Msg) { msg.TxID, msg.NetworkInternalCode, ec.signingResultQueue, + msg.DerivationPath, idempotentKey, ) default: @@ -726,7 +728,7 @@ func (ec *eventConsumer) consumeReshareEvent() error { wg.Wait() logger.Info("Reshare session finished", "walletID", walletID, "pubKey", fmt.Sprintf("%x", successEvent.PubKey)) - if newSession != nil { + if newSession != nil && len(successEvent.PubKey) > 0 { successBytes, err := json.Marshal(successEvent) if err != nil { logger.Error("Failed to marshal reshare success event", err) diff --git a/pkg/mpc/ckd.go b/pkg/mpc/ckd.go new file mode 100644 index 0000000..2b452b8 --- /dev/null +++ b/pkg/mpc/ckd.go @@ -0,0 +1,125 @@ +package mpc + +import ( + "crypto/elliptic" + "encoding/hex" + "errors" + "fmt" + "math/big" + + "github.com/bnb-chain/tss-lib/v2/crypto" + "github.com/bnb-chain/tss-lib/v2/crypto/ckd" + ecdsaKeygen "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" + eddsaKeygen "github.com/bnb-chain/tss-lib/v2/eddsa/keygen" + "github.com/btcsuite/btcd/chaincfg" +) + +const chainCodeLength = 32 + +var ( + ErrInvalidChainCode = errors.New("invalid chain code length") + ErrNilKey = errors.New("key cannot be nil") + ErrNilPoint = errors.New("point cannot be nil") +) + +// CKD handles Child Key Derivation (ENV-based) +type CKD struct { + masterChainCode []byte +} + +// NewCKDFromHex creates CKD from a hex-encoded chain code string (32 bytes). +func NewCKDFromHex(hexStr string) (*CKD, error) { + if hexStr == "" { + return nil, fmt.Errorf("chain code is empty") + } + code, err := hex.DecodeString(hexStr) + if err != nil { + return nil, fmt.Errorf("invalid chain code hex: %w", err) + } + if len(code) != chainCodeLength { + return nil, fmt.Errorf("%w: got %d, want %d", ErrInvalidChainCode, len(code), chainCodeLength) + } + return &CKD{masterChainCode: code}, nil +} + +// GetMasterChainCode returns a copy of the chain code. +func (c *CKD) GetMasterChainCode() []byte { + out := make([]byte, len(c.masterChainCode)) + copy(out, c.masterChainCode) + return out +} + +// Derive derives a child key from the master public key using the given path. +// Uses standard BIP32 derivation: same master key + same path = same child key. +// Each level in the path automatically gets its own chain code via HMAC(parent_chain_code, pubkey || index). +func (c *CKD) Derive(walletID string, masterPub *crypto.ECPoint, path []uint32, curve elliptic.Curve) (*big.Int, *ckd.ExtendedKey, error) { + if masterPub == nil { + return nil, nil, ErrNilPoint + } + if curve == nil { + return nil, nil, errors.New("curve cannot be nil") + } + + masterCC := append([]byte(nil), c.masterChainCode...) + + return c.derivingPubkeyFromPath(masterPub, masterCC, path, curve) +} + +// derivingPubkeyFromPath performs the actual derivation. +func (c *CKD) derivingPubkeyFromPath(masterPub *crypto.ECPoint, chainCode []byte, path []uint32, ec elliptic.Curve) (*big.Int, *ckd.ExtendedKey, error) { + net := &chaincfg.MainNetParams + parent := &ckd.ExtendedKey{ + PublicKey: masterPub, + Depth: 0, + ChildIndex: 0, + ChainCode: chainCode, + ParentFP: []byte{0x00, 0x00, 0x00, 0x00}, + Version: net.HDPrivateKeyID[:], + } + + delta, extKey, err := ckd.DeriveChildKeyFromHierarchy(path, parent, ec.Params().N, ec) + if err != nil { + return nil, nil, fmt.Errorf("failed to derive child key: %w", err) + } + return delta, extKey, nil +} + +// ECDSAUpdateSinglePublicKeyAndAdjustBigXj updates ECDSA public key and BigXj. +func (c *CKD) ECDSAUpdateSinglePublicKeyAndAdjustBigXj(delta *big.Int, key *ecdsaKeygen.LocalPartySaveData, childPk *crypto.ECPoint, ec elliptic.Curve) error { + if key == nil { + return ErrNilKey + } + if childPk == nil { + return ErrNilPoint + } + gDelta := crypto.ScalarBaseMult(ec, delta) + key.ECDSAPub = childPk + for i := range key.BigXj { + updated, err := key.BigXj[i].Add(gDelta) + if err != nil { + return fmt.Errorf("failed to update BigXj[%d]: %w", i, err) + } + key.BigXj[i] = updated + } + return nil +} + +// EDDSAUpdateSinglePublicKeyAndAdjustBigXj updates EdDSA public key and BigXj. +func (c *CKD) EDDSAUpdateSinglePublicKeyAndAdjustBigXj(delta *big.Int, key *eddsaKeygen.LocalPartySaveData, childPk *crypto.ECPoint, ec elliptic.Curve) error { + if key == nil { + return ErrNilKey + } + if childPk == nil { + return ErrNilPoint + } + gDelta := crypto.ScalarBaseMult(ec, delta) + key.EDDSAPub = childPk + for i := range key.BigXj { + updated, err := key.BigXj[i].Add(gDelta) + if err != nil { + return fmt.Errorf("failed to update BigXj[%d]: %w", i, err) + } + key.BigXj[i] = updated + } + return nil +} diff --git a/pkg/mpc/ecdsa_signing_session.go b/pkg/mpc/ecdsa_signing_session.go index d8fc3b5..60dec8a 100644 --- a/pkg/mpc/ecdsa_signing_session.go +++ b/pkg/mpc/ecdsa_signing_session.go @@ -35,6 +35,8 @@ type ecdsaSigningSession struct { tx *big.Int txID string networkInternalCode string + derivationPath []uint32 + ckd *CKD } func newECDSASigningSession( @@ -52,8 +54,11 @@ func newECDSASigningSession( keyinfoStore keyinfo.Store, resultQueue messaging.MessageQueue, identityStore identity.Store, + derivationPath []uint32, idempotentKey string, + ckd *CKD, ) *ecdsaSigningSession { + return &ecdsaSigningSession{ session: session{ walletID: walletID, @@ -87,7 +92,10 @@ func newECDSASigningSession( endCh: make(chan *common.SignatureData), txID: txID, networkInternalCode: networkInternalCode, + derivationPath: derivationPath, + ckd: ckd, } + } func (s *ecdsaSigningSession) Init(tx *big.Int) error { @@ -128,7 +136,22 @@ func (s *ecdsaSigningSession) Init(tx *big.Int) error { return errors.Wrap(err, "Failed to unmarshal wallet data") } - s.party = signing.NewLocalParty(tx, params, data, s.outCh, s.endCh) + if len(s.derivationPath) > 0 { + il, extendedChildPk, errorDerivation := s.ckd.Derive(s.walletID, data.ECDSAPub, s.derivationPath, tss.S256()) + if errorDerivation != nil { + return errors.Wrap(errorDerivation, fmt.Sprintf("Failed to derive key, derivationPath: %v", s.derivationPath)) + } + keyDerivationDelta := il + err = s.ckd.ECDSAUpdateSinglePublicKeyAndAdjustBigXj(keyDerivationDelta, &data, extendedChildPk.PublicKey, tss.S256()) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to update public key, derivationPath: %v", s.derivationPath)) + } + + s.party = signing.NewLocalPartyWithKDD(tx, params, data, keyDerivationDelta, s.outCh, s.endCh, 0) + + } else { + s.party = signing.NewLocalParty(tx, params, data, s.outCh, s.endCh) + } s.data = &data s.version = keyInfo.Version s.tx = tx diff --git a/pkg/mpc/eddsa_signing_session.go b/pkg/mpc/eddsa_signing_session.go index d70b242..f4df093 100644 --- a/pkg/mpc/eddsa_signing_session.go +++ b/pkg/mpc/eddsa_signing_session.go @@ -27,6 +27,8 @@ type eddsaSigningSession struct { tx *big.Int txID string networkInternalCode string + derivationPath []uint32 + ckd *CKD } func newEDDSASigningSession( @@ -43,7 +45,9 @@ func newEDDSASigningSession( keyinfoStore keyinfo.Store, resultQueue messaging.MessageQueue, identityStore identity.Store, + derivationPath []uint32, idempotentKey string, + ckd *CKD, ) *eddsaSigningSession { return &eddsaSigningSession{ session: session{ @@ -78,6 +82,8 @@ func newEDDSASigningSession( endCh: make(chan *common.SignatureData), txID: txID, networkInternalCode: networkInternalCode, + derivationPath: derivationPath, + ckd: ckd, } } @@ -119,7 +125,22 @@ func (s *eddsaSigningSession) Init(tx *big.Int) error { return errors.Wrap(err, "Failed to unmarshal wallet data") } - s.party = signing.NewLocalParty(tx, params, data, s.outCh, s.endCh) + if len(s.derivationPath) > 0 { + il, extendedChildPk, errorDerivation := s.ckd.Derive(s.walletID, data.EDDSAPub, s.derivationPath, tss.Edwards()) + if errorDerivation != nil { + return errors.Wrap(errorDerivation, fmt.Sprintf("Failed to derive key, derivationPath: %v", s.derivationPath)) + } + keyDerivationDelta := il + err = s.ckd.EDDSAUpdateSinglePublicKeyAndAdjustBigXj(keyDerivationDelta, &data, extendedChildPk.PublicKey, tss.Edwards()) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to update public key, derivationPath: %v", s.derivationPath)) + } + + s.party = signing.NewLocalPartyWithKDD(tx, params, data, keyDerivationDelta, s.outCh, s.endCh, 0) + + } else { + s.party = signing.NewLocalParty(tx, params, data, s.outCh, s.endCh) + } s.data = &data s.version = keyInfo.Version s.tx = tx diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index d615444..e03bd01 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -37,8 +37,8 @@ type Node struct { keyinfoStore keyinfo.Store ecdsaPreParams []*keygen.LocalPreParams identityStore identity.Store - - peerRegistry PeerRegistry + peerRegistry PeerRegistry + ckd *CKD } func NewNode( @@ -50,6 +50,7 @@ func NewNode( keyinfoStore keyinfo.Store, peerRegistry PeerRegistry, identityStore identity.Store, + ckd *CKD, ) *Node { start := time.Now() elapsed := time.Since(start) @@ -64,6 +65,7 @@ func NewNode( keyinfoStore: keyinfoStore, peerRegistry: peerRegistry, identityStore: identityStore, + ckd: ckd, } node.ecdsaPreParams = node.generatePreParams() @@ -146,6 +148,7 @@ func (p *Node) CreateSigningSession( txID string, networkInternalCode string, resultQueue messaging.MessageQueue, + derivationPath []uint32, idempotentKey string, ) (SigningSession, error) { version := p.getVersion(sessionType, walletID) @@ -193,7 +196,9 @@ func (p *Node) CreateSigningSession( p.keyinfoStore, resultQueue, p.identityStore, + derivationPath, idempotentKey, + p.ckd, ), nil case SessionTypeEDDSA: @@ -211,7 +216,9 @@ func (p *Node) CreateSigningSession( p.keyinfoStore, resultQueue, p.identityStore, + derivationPath, idempotentKey, + p.ckd, ), nil } diff --git a/pkg/types/initiator_msg.go b/pkg/types/initiator_msg.go index e1ddcb4..6100492 100644 --- a/pkg/types/initiator_msg.go +++ b/pkg/types/initiator_msg.go @@ -47,6 +47,7 @@ type SignTxMessage struct { TxID string `json:"tx_id"` Tx []byte `json:"tx"` Signature []byte `json:"signature"` + DerivationPath []uint32 `json:"derivation_path"` AuthorizerSignatures []AuthorizerSignature `json:"authorizer_signatures,omitempty"` } diff --git a/setup.sh b/setup.sh index 92c6f40..7be28d3 100755 --- a/setup.sh +++ b/setup.sh @@ -1,5 +1,7 @@ NUM_NODES=3 +make build + echo "🚀 Start the services..." docker compose up -d sleep 3 diff --git a/setup_identities.sh b/setup_identities.sh index 3b3cd07..7ca5ed4 100755 --- a/setup_identities.sh +++ b/setup_identities.sh @@ -9,7 +9,17 @@ echo "🚀 Setting up Node Identities..." # Preconditions command -v mpcium-cli >/dev/null 2>&1 || { echo "❌ mpcium-cli not found in PATH"; exit 1; } [ -f config.yaml ] || { echo "❌ config.yaml not found in repo root"; exit 1; } -[ -f peers.json ] || { echo "❌ peers.json not found in repo root"; exit 1; } + +# Check if peers.json exists, if not provide helpful instructions +if [ ! -f peers.json ]; then + echo "❌ peers.json not found in repo root" + echo "" + echo "📝 Please generate peers.json first by running:" + echo " mpcium-cli generate-peers -n $NUM_NODES" + echo "" + echo "This will create a peers.json file with $NUM_NODES peer nodes." + exit 1 +fi # Create node directories and copy config files echo "📁 Creating node directories..." @@ -30,6 +40,53 @@ for i in $(seq 0 $((NUM_NODES-1))); do ( cd "node$i" && mpcium-cli generate-identity --node "node$i" ) done +# Generate a single chain_code if not present and set it in configs +if [ ! -f .chain_code ]; then + echo "🔐 Generating chain_code (32-byte hex) ..." + CC=$(openssl rand -hex 32) + echo "$CC" > .chain_code +else + CC=$(cat .chain_code) +fi + +if [ -z "$CC" ]; then + echo "❌ Failed to determine chain_code" + exit 1 +fi + +echo "📝 Setting chain_code in root config.yaml ..." +if grep -q '^\s*chain_code:' config.yaml; then + sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" config.yaml +else + printf '\nchain_code: "%s"\n' "$CC" >> config.yaml +fi + +echo "📦 Distributing chain_code to node configs ..." +for i in $(seq 0 $((NUM_NODES-1))); do + if grep -q '^\s*chain_code:' "node$i/config.yaml"; then + sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" "node$i/config.yaml" + else + printf '\nchain_code: "%s"\n' "$CC" >> "node$i/config.yaml" + fi +done + +# Distribute event_initiator_pubkey to all node configs +if [ -f "event_initiator.identity.json" ]; then + INITIATOR_PUBKEY=$(grep -o '"public_key": *"[^"]*"' event_initiator.identity.json | cut -d '"' -f4) + if [ -n "${INITIATOR_PUBKEY}" ]; then + echo "📦 Distributing event_initiator_pubkey to node configs ..." + for i in $(seq 0 $((NUM_NODES-1))); do + if grep -q '^\s*event_initiator_pubkey:' "node$i/config.yaml"; then + if [[ "${OSTYPE:-}" == darwin* ]]; then + sed -i '' -E "s|^([[:space:]]*event_initiator_pubkey:).*|\1 \"${INITIATOR_PUBKEY}\"|" "node$i/config.yaml" + else + sed -i -E "s|^([[:space:]]*event_initiator_pubkey:).*|\1 \"${INITIATOR_PUBKEY}\"|" "node$i/config.yaml" + fi + fi + done + fi +fi + # Distribute identity files to all nodes echo "🔄 Distributing identity files across nodes..." for i in $(seq 0 $((NUM_NODES-1))); do