From 602eaec6acfeafa2cb178453185a3e260f4390a8 Mon Sep 17 00:00:00 2001 From: vietddude Date: Fri, 12 Jul 2024 14:30:00 +0700 Subject: [PATCH 01/21] WIP: Add CKD implementation and integrate derivation path in signing sessions use consulKV for store ckd seed --- examples/ckd/main.go | 102 +++++++++++++++++++++ pkg/eventconsumer/event_consumer.go | 2 + pkg/mpc/ckd.go | 137 ++++++++++++++++++++++++++++ pkg/mpc/ecdsa_signing_session.go | 25 ++++- pkg/mpc/eddsa_signing_session.go | 3 + pkg/mpc/node.go | 3 + pkg/types/initiator_msg.go | 1 + 7 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 examples/ckd/main.go create mode 100644 pkg/mpc/ckd.go 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/pkg/eventconsumer/event_consumer.go b/pkg/eventconsumer/event_consumer.go index af21881..c010307 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: diff --git a/pkg/mpc/ckd.go b/pkg/mpc/ckd.go new file mode 100644 index 0000000..618d43f --- /dev/null +++ b/pkg/mpc/ckd.go @@ -0,0 +1,137 @@ +package mpc + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + "math/big" + + "github.com/bnb-chain/tss-lib/v2/common" + "github.com/bnb-chain/tss-lib/v2/crypto" + "github.com/bnb-chain/tss-lib/v2/crypto/ckd" + "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" + "github.com/fystack/mpcium/pkg/infra" + "github.com/fystack/mpcium/pkg/logger" + "github.com/hashicorp/consul/api" + + "github.com/btcsuite/btcd/chaincfg" +) + +// Child Key Derivation +type CKD struct { + Store infra.ConsulKV + ChainCode []byte + Path []uint32 +} + +func NewCKD() *CKD { + ckd := &CKD{ + Store: infra.GetConsulClient("development").KV(), + } + ckd.initializeChainCode() + return ckd +} + +func (c *CKD) UpdateSinglePublicKeyAndAdjustBigXj( + keyDerivationDelta *big.Int, + key *keygen.LocalPartySaveData, + extendedChildPk *ecdsa.PublicKey, + ec elliptic.Curve, +) error { + var err error + + // Compute g^delta + gDelta := crypto.ScalarBaseMult(ec, keyDerivationDelta) + + // Update the public key + key.ECDSAPub, err = crypto.NewECPoint(ec, extendedChildPk.X, extendedChildPk.Y) + if err != nil { + common.Logger.Errorf("error creating new extended child public key") + return err + } + + // Update each BigXj[i] := BigXj[i] + g^delta + for j := range key.BigXj { + key.BigXj[j], err = key.BigXj[j].Add(gDelta) + if err != nil { + common.Logger.Errorf("error in delta operation") + return err + } + } + + return nil +} + +func (c *CKD) Derive(masterPub *crypto.ECPoint, path []uint32, curve elliptic.Curve) (*big.Int, *ckd.ExtendedKey, error) { + return c.derivingPubkeyFromPath(masterPub, c.ChainCode, path, curve) +} + +func (c *CKD) derivingPubkeyFromPath(masterPub *crypto.ECPoint, chainCode []byte, path []uint32, ec elliptic.Curve) (*big.Int, *ckd.ExtendedKey, error) { + // build ecdsa key pair + pk := ecdsa.PublicKey{ + Curve: ec, + X: masterPub.X(), + Y: masterPub.Y(), + } + + net := &chaincfg.MainNetParams + extendedParentPk := &ckd.ExtendedKey{ + PublicKey: pk, + Depth: 0, + ChildIndex: 0, + ChainCode: chainCode[:], + ParentFP: []byte{0x00, 0x00, 0x00, 0x00}, + Version: net.HDPrivateKeyID[:], + } + + return ckd.DeriveChildKeyFromHierarchy(path, extendedParentPk, ec.Params().N, ec) +} + +func (c *CKD) initializeChainCode() error { + logger.Info("Initializing chain code") + + // Try to get chain code from store + val, _, err := c.Store.Get("chain_code", nil) + if err == nil && val != nil && len(val.Value) == 32 { + // Found existing chain code + c.ChainCode = make([]byte, 32) + copy(c.ChainCode, val.Value) + logger.Info("Loaded existing chain code", "chainCode", c.ChainCode) + return nil + } + + // Not found or invalid: generate new chain code + chainCode := make([]byte, 32) + max := new(big.Int).Lsh(big.NewInt(1), 256) + max.Sub(max, big.NewInt(1)) + fillBytes(common.GetRandomPositiveInt(rand.Reader, max), chainCode) + + // Save to store + _, err = c.Store.Put(&api.KVPair{Key: "chain_code", Value: chainCode}, nil) + if err != nil { + return fmt.Errorf("failed to store chain code: %w", err) + } + + // Assign to CKD struct + c.ChainCode = make([]byte, 32) + copy(c.ChainCode, chainCode) + logger.Info("Generated new chain code", "chainCode", c.ChainCode) + return nil +} + +func fillBytes(x *big.Int, buf []byte) []byte { + b := x.Bytes() + if len(b) > len(buf) { + panic("buffer too small") + } + offset := len(buf) - len(b) + for i := range buf { + if i < offset { + buf[i] = 0 + } else { + buf[i] = b[i-offset] + } + } + return buf +} diff --git a/pkg/mpc/ecdsa_signing_session.go b/pkg/mpc/ecdsa_signing_session.go index d8fc3b5..3b868fb 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,10 @@ func newECDSASigningSession( keyinfoStore keyinfo.Store, resultQueue messaging.MessageQueue, identityStore identity.Store, + derivationPath []uint32, idempotentKey string, ) *ecdsaSigningSession { + return &ecdsaSigningSession{ session: session{ walletID: walletID, @@ -87,7 +91,10 @@ func newECDSASigningSession( endCh: make(chan *common.SignatureData), txID: txID, networkInternalCode: networkInternalCode, + derivationPath: derivationPath, + ckd: NewCKD(), } + } func (s *ecdsaSigningSession) Init(tx *big.Int) error { @@ -128,7 +135,23 @@ 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 { + logger.Info("Deriving key from derivation path", "derivationPath", s.derivationPath) + il, extendedChildPk, errorDerivation := s.ckd.Derive(data.ECDSAPub, s.derivationPath, tss.S256()) + if errorDerivation != nil { + return errors.Wrap(errorDerivation, "Failed to derive key") + } + keyDerivationDelta := il + err = s.ckd.UpdateSinglePublicKeyAndAdjustBigXj(keyDerivationDelta, &data, &extendedChildPk.PublicKey, tss.S256()) + if err != nil { + return errors.Wrap(err, "Failed to update public key") + } + + 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..bd7d8a3 100644 --- a/pkg/mpc/eddsa_signing_session.go +++ b/pkg/mpc/eddsa_signing_session.go @@ -27,6 +27,7 @@ type eddsaSigningSession struct { tx *big.Int txID string networkInternalCode string + derivationPath []uint32 } func newEDDSASigningSession( @@ -43,6 +44,7 @@ func newEDDSASigningSession( keyinfoStore keyinfo.Store, resultQueue messaging.MessageQueue, identityStore identity.Store, + derivationPath []uint32, idempotentKey string, ) *eddsaSigningSession { return &eddsaSigningSession{ @@ -78,6 +80,7 @@ func newEDDSASigningSession( endCh: make(chan *common.SignatureData), txID: txID, networkInternalCode: networkInternalCode, + derivationPath: derivationPath, } } diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index d615444..7c7180e 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -146,6 +146,7 @@ func (p *Node) CreateSigningSession( txID string, networkInternalCode string, resultQueue messaging.MessageQueue, + derivationPath []uint32, idempotentKey string, ) (SigningSession, error) { version := p.getVersion(sessionType, walletID) @@ -193,6 +194,7 @@ func (p *Node) CreateSigningSession( p.keyinfoStore, resultQueue, p.identityStore, + derivationPath, idempotentKey, ), nil @@ -211,6 +213,7 @@ func (p *Node) CreateSigningSession( p.keyinfoStore, resultQueue, p.identityStore, + derivationPath, idempotentKey, ), 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"` } From 43d0a41c34f81e641bc99f92a8583cc1d3244809 Mon Sep 17 00:00:00 2001 From: vietddude Date: Wed, 23 Jul 2025 17:34:46 +0700 Subject: [PATCH 02/21] Update .gitignore to exclude node modules and modify CKD example --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cdc1fb9..21e6163 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 From 0a143f640ec387e06415d83894a5a073fabaaeca Mon Sep 17 00:00:00 2001 From: vietddude Date: Wed, 23 Jul 2025 17:39:22 +0700 Subject: [PATCH 03/21] chore: fix lint error --- pkg/mpc/ckd.go | 5 ++++- pkg/mpc/node.go | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/mpc/ckd.go b/pkg/mpc/ckd.go index 618d43f..b9de885 100644 --- a/pkg/mpc/ckd.go +++ b/pkg/mpc/ckd.go @@ -29,7 +29,10 @@ func NewCKD() *CKD { ckd := &CKD{ Store: infra.GetConsulClient("development").KV(), } - ckd.initializeChainCode() + err := ckd.initializeChainCode() + if err != nil { + logger.Fatal("Failed to initialize chain code", err) + } return ckd } diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index 7c7180e..f97d336 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -1,6 +1,7 @@ package mpc import ( + "bytes" "encoding/json" "fmt" "slices" @@ -37,8 +38,15 @@ type Node struct { keyinfoStore keyinfo.Store ecdsaPreParams []*keygen.LocalPreParams identityStore identity.Store + peerRegistry PeerRegistry +} + +func PartyIDToRoutingDest(partyID *tss.PartyID) string { + return string(partyID.KeyInt().Bytes()) +} - peerRegistry PeerRegistry +func ComparePartyIDs(x, y *tss.PartyID) bool { + return bytes.Equal(x.KeyInt().Bytes(), y.KeyInt().Bytes()) } func NewNode( From 351618b04e687bf6be6ccb7b0596e62836a2e282 Mon Sep 17 00:00:00 2001 From: vietddude Date: Tue, 28 Oct 2025 16:04:58 +0700 Subject: [PATCH 04/21] feat: support eddsa ckd --- cmd/mpcium/main.go | 6 + go.mod | 5 +- go.sum | 12 +- pkg/mpc/ckd.go | 182 +++++++++++++++---------------- pkg/mpc/ecdsa_signing_session.go | 5 +- pkg/mpc/eddsa_signing_session.go | 21 +++- pkg/mpc/node.go | 5 + 7 files changed, 130 insertions(+), 106 deletions(-) diff --git a/cmd/mpcium/main.go b/cmd/mpcium/main.go index 85383f9..be851be 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -203,6 +203,11 @@ func runNode(ctx context.Context, c *cli.Command) error { peerNodeIDs := GetPeerIDs(peers) peerRegistry := mpc.NewRegistry(nodeID, peerNodeIDs, consulClient.KV(), directMessaging, pubsub, identityStore) + ckd, err := mpc.NewCKD() + if err != nil { + logger.Fatal("Failed to create ckd store", err) + } + mpcNode := mpc.NewNode( nodeID, peerNodeIDs, @@ -212,6 +217,7 @@ func runNode(ctx context.Context, c *cli.Command) error { keyinfoStore, peerRegistry, identityStore, + ckd, ) defer mpcNode.Close() diff --git a/go.mod b/go.mod index d9cc99b..c515c5d 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ 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/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,7 +41,6 @@ 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 @@ -102,8 +102,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/vietddude/tss-lib/v2 v2.0.1 diff --git a/go.sum b/go.sum index da2768e..a3dfd2a 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= @@ -158,13 +156,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= @@ -226,6 +221,7 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -368,6 +364,8 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o= github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/vietddude/tss-lib/v2 v2.0.1 h1:mP8hXAxgjvZk3hgNulDigg4ws8OG0X2SvS0VcLk6EAk= +github.com/vietddude/tss-lib/v2 v2.0.1/go.mod h1:CaMK1w/NovT3qXfoa90o4Sc50yjjQW8r5BgVe9FOMMw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -407,8 +405,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= diff --git a/pkg/mpc/ckd.go b/pkg/mpc/ckd.go index b9de885..3aa965d 100644 --- a/pkg/mpc/ckd.go +++ b/pkg/mpc/ckd.go @@ -1,140 +1,136 @@ package mpc import ( - "crypto/ecdsa" "crypto/elliptic" - "crypto/rand" + "encoding/hex" + "errors" "fmt" "math/big" + "os" + "sync" - "github.com/bnb-chain/tss-lib/v2/common" "github.com/bnb-chain/tss-lib/v2/crypto" "github.com/bnb-chain/tss-lib/v2/crypto/ckd" - "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" - "github.com/fystack/mpcium/pkg/infra" + 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" "github.com/fystack/mpcium/pkg/logger" - "github.com/hashicorp/consul/api" +) - "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") ) -// Child Key Derivation +// CKD handles Child Key Derivation (ENV-based) type CKD struct { - Store infra.ConsulKV - ChainCode []byte - Path []uint32 + chainCode []byte + mu sync.RWMutex } -func NewCKD() *CKD { - ckd := &CKD{ - Store: infra.GetConsulClient("development").KV(), +// NewCKD loads chain code from environment variable CHAIN_CODE (hex-encoded). +func NewCKD() (*CKD, error) { + envVal := os.Getenv("CHAIN_CODE") + if envVal == "" { + return nil, fmt.Errorf("CHAIN_CODE not set in environment") } - err := ckd.initializeChainCode() + + code, err := hex.DecodeString(envVal) if err != nil { - logger.Fatal("Failed to initialize chain code", err) + return nil, fmt.Errorf("invalid CHAIN_CODE hex: %w", err) } - return ckd -} + if len(code) != chainCodeLength { + return nil, fmt.Errorf("%w: got %d, want %d", ErrInvalidChainCode, len(code), chainCodeLength) + } + + logger.Info("Loaded static chain code from environment") -func (c *CKD) UpdateSinglePublicKeyAndAdjustBigXj( - keyDerivationDelta *big.Int, - key *keygen.LocalPartySaveData, - extendedChildPk *ecdsa.PublicKey, - ec elliptic.Curve, -) error { - var err error + return &CKD{chainCode: code}, nil +} - // Compute g^delta - gDelta := crypto.ScalarBaseMult(ec, keyDerivationDelta) +// GetChainCode returns a copy of the chain code. +func (c *CKD) GetChainCode() []byte { + c.mu.RLock() + defer c.mu.RUnlock() + out := make([]byte, len(c.chainCode)) + copy(out, c.chainCode) + return out +} - // Update the public key - key.ECDSAPub, err = crypto.NewECPoint(ec, extendedChildPk.X, extendedChildPk.Y) - if err != nil { - common.Logger.Errorf("error creating new extended child public key") - return err +// Derive derives a child key from the master public key using the given path. +func (c *CKD) Derive(masterPub *crypto.ECPoint, path []uint32, curve elliptic.Curve) (*big.Int, *ckd.ExtendedKey, error) { + if masterPub == nil { + return nil, nil, ErrNilPoint } - - // Update each BigXj[i] := BigXj[i] + g^delta - for j := range key.BigXj { - key.BigXj[j], err = key.BigXj[j].Add(gDelta) - if err != nil { - common.Logger.Errorf("error in delta operation") - return err - } + if curve == nil { + return nil, nil, errors.New("curve cannot be nil") } - return nil -} + c.mu.RLock() + cc := append([]byte(nil), c.chainCode...) + c.mu.RUnlock() -func (c *CKD) Derive(masterPub *crypto.ECPoint, path []uint32, curve elliptic.Curve) (*big.Int, *ckd.ExtendedKey, error) { - return c.derivingPubkeyFromPath(masterPub, c.ChainCode, path, curve) + return c.derivingPubkeyFromPath(masterPub, cc, 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) { - // build ecdsa key pair - pk := ecdsa.PublicKey{ - Curve: ec, - X: masterPub.X(), - Y: masterPub.Y(), - } - net := &chaincfg.MainNetParams - extendedParentPk := &ckd.ExtendedKey{ - PublicKey: pk, + parent := &ckd.ExtendedKey{ + PublicKey: masterPub, Depth: 0, ChildIndex: 0, - ChainCode: chainCode[:], + ChainCode: chainCode, ParentFP: []byte{0x00, 0x00, 0x00, 0x00}, Version: net.HDPrivateKeyID[:], } - return ckd.DeriveChildKeyFromHierarchy(path, extendedParentPk, ec.Params().N, ec) + 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 } -func (c *CKD) initializeChainCode() error { - logger.Info("Initializing chain code") - - // Try to get chain code from store - val, _, err := c.Store.Get("chain_code", nil) - if err == nil && val != nil && len(val.Value) == 32 { - // Found existing chain code - c.ChainCode = make([]byte, 32) - copy(c.ChainCode, val.Value) - logger.Info("Loaded existing chain code", "chainCode", c.ChainCode) - return 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 } - - // Not found or invalid: generate new chain code - chainCode := make([]byte, 32) - max := new(big.Int).Lsh(big.NewInt(1), 256) - max.Sub(max, big.NewInt(1)) - fillBytes(common.GetRandomPositiveInt(rand.Reader, max), chainCode) - - // Save to store - _, err = c.Store.Put(&api.KVPair{Key: "chain_code", Value: chainCode}, nil) - if err != nil { - return fmt.Errorf("failed to store chain code: %w", err) + 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 } - - // Assign to CKD struct - c.ChainCode = make([]byte, 32) - copy(c.ChainCode, chainCode) - logger.Info("Generated new chain code", "chainCode", c.ChainCode) return nil } -func fillBytes(x *big.Int, buf []byte) []byte { - b := x.Bytes() - if len(b) > len(buf) { - panic("buffer too small") +// 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 } - offset := len(buf) - len(b) - for i := range buf { - if i < offset { - buf[i] = 0 - } else { - buf[i] = b[i-offset] + 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 buf + return nil } diff --git a/pkg/mpc/ecdsa_signing_session.go b/pkg/mpc/ecdsa_signing_session.go index 3b868fb..75b3f27 100644 --- a/pkg/mpc/ecdsa_signing_session.go +++ b/pkg/mpc/ecdsa_signing_session.go @@ -56,6 +56,7 @@ func newECDSASigningSession( identityStore identity.Store, derivationPath []uint32, idempotentKey string, + ckd *CKD, ) *ecdsaSigningSession { return &ecdsaSigningSession{ @@ -92,7 +93,7 @@ func newECDSASigningSession( txID: txID, networkInternalCode: networkInternalCode, derivationPath: derivationPath, - ckd: NewCKD(), + ckd: ckd, } } @@ -142,7 +143,7 @@ func (s *ecdsaSigningSession) Init(tx *big.Int) error { return errors.Wrap(errorDerivation, "Failed to derive key") } keyDerivationDelta := il - err = s.ckd.UpdateSinglePublicKeyAndAdjustBigXj(keyDerivationDelta, &data, &extendedChildPk.PublicKey, tss.S256()) + err = s.ckd.ECDSAUpdateSinglePublicKeyAndAdjustBigXj(keyDerivationDelta, &data, extendedChildPk.PublicKey, tss.S256()) if err != nil { return errors.Wrap(err, "Failed to update public key") } diff --git a/pkg/mpc/eddsa_signing_session.go b/pkg/mpc/eddsa_signing_session.go index bd7d8a3..8921c1f 100644 --- a/pkg/mpc/eddsa_signing_session.go +++ b/pkg/mpc/eddsa_signing_session.go @@ -28,6 +28,7 @@ type eddsaSigningSession struct { txID string networkInternalCode string derivationPath []uint32 + ckd *CKD } func newEDDSASigningSession( @@ -46,6 +47,7 @@ func newEDDSASigningSession( identityStore identity.Store, derivationPath []uint32, idempotentKey string, + ckd *CKD, ) *eddsaSigningSession { return &eddsaSigningSession{ session: session{ @@ -81,6 +83,7 @@ func newEDDSASigningSession( txID: txID, networkInternalCode: networkInternalCode, derivationPath: derivationPath, + ckd: ckd, } } @@ -122,7 +125,23 @@ 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 { + logger.Info("Deriving key from derivation path", "derivationPath", s.derivationPath) + il, extendedChildPk, errorDerivation := s.ckd.Derive(data.EDDSAPub, s.derivationPath, tss.Edwards()) + if errorDerivation != nil { + return errors.Wrap(errorDerivation, "Failed to derive key") + } + keyDerivationDelta := il + err = s.ckd.EDDSAUpdateSinglePublicKeyAndAdjustBigXj(keyDerivationDelta, &data, extendedChildPk.PublicKey, tss.Edwards()) + if err != nil { + return errors.Wrap(err, "Failed to update public key") + } + + 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 f97d336..6e8a7e3 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -39,6 +39,7 @@ type Node struct { ecdsaPreParams []*keygen.LocalPreParams identityStore identity.Store peerRegistry PeerRegistry + ckd *CKD } func PartyIDToRoutingDest(partyID *tss.PartyID) string { @@ -58,6 +59,7 @@ func NewNode( keyinfoStore keyinfo.Store, peerRegistry PeerRegistry, identityStore identity.Store, + ckd *CKD, ) *Node { start := time.Now() elapsed := time.Since(start) @@ -72,6 +74,7 @@ func NewNode( keyinfoStore: keyinfoStore, peerRegistry: peerRegistry, identityStore: identityStore, + ckd: ckd, } node.ecdsaPreParams = node.generatePreParams() @@ -204,6 +207,7 @@ func (p *Node) CreateSigningSession( p.identityStore, derivationPath, idempotentKey, + p.ckd, ), nil case SessionTypeEDDSA: @@ -223,6 +227,7 @@ func (p *Node) CreateSigningSession( p.identityStore, derivationPath, idempotentKey, + p.ckd, ), nil } From 72e6bce15a343bd8ec89729135fb0a6f9f61d051 Mon Sep 17 00:00:00 2001 From: vietddude Date: Fri, 31 Oct 2025 11:28:25 +0700 Subject: [PATCH 05/21] CKD struct refactor: rename chain code field to masterChainCode and adjust constructor/accessor --- pkg/mpc/ckd.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/mpc/ckd.go b/pkg/mpc/ckd.go index 3aa965d..9e6f4cd 100644 --- a/pkg/mpc/ckd.go +++ b/pkg/mpc/ckd.go @@ -27,8 +27,8 @@ var ( // CKD handles Child Key Derivation (ENV-based) type CKD struct { - chainCode []byte - mu sync.RWMutex + masterChainCode []byte + mu sync.RWMutex } // NewCKD loads chain code from environment variable CHAIN_CODE (hex-encoded). @@ -48,20 +48,20 @@ func NewCKD() (*CKD, error) { logger.Info("Loaded static chain code from environment") - return &CKD{chainCode: code}, nil + return &CKD{masterChainCode: code}, nil } -// GetChainCode returns a copy of the chain code. -func (c *CKD) GetChainCode() []byte { +// GetMasterChainCode returns a copy of the chain code. +func (c *CKD) GetMasterChainCode() []byte { c.mu.RLock() defer c.mu.RUnlock() - out := make([]byte, len(c.chainCode)) - copy(out, c.chainCode) + 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. -func (c *CKD) Derive(masterPub *crypto.ECPoint, path []uint32, curve elliptic.Curve) (*big.Int, *ckd.ExtendedKey, error) { +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 } From d6b49921235d2ec5ce792d17c27e704da717ff08 Mon Sep 17 00:00:00 2001 From: vietddude Date: Fri, 31 Oct 2025 11:28:25 +0700 Subject: [PATCH 06/21] Enable wallet-scoped derivation: add walletID to Derive and wire through HMAC-SHA512 --- pkg/mpc/ckd.go | 10 ++++++++-- pkg/mpc/ecdsa_signing_session.go | 2 +- pkg/mpc/eddsa_signing_session.go | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/mpc/ckd.go b/pkg/mpc/ckd.go index 9e6f4cd..7d24a95 100644 --- a/pkg/mpc/ckd.go +++ b/pkg/mpc/ckd.go @@ -2,6 +2,8 @@ package mpc import ( "crypto/elliptic" + "crypto/hmac" + "crypto/sha512" "encoding/hex" "errors" "fmt" @@ -70,10 +72,14 @@ func (c *CKD) Derive(walletID string, masterPub *crypto.ECPoint, path []uint32, } c.mu.RLock() - cc := append([]byte(nil), c.chainCode...) + masterCC := append([]byte(nil), c.masterChainCode...) c.mu.RUnlock() - return c.derivingPubkeyFromPath(masterPub, cc, path, curve) + h := hmac.New(sha512.New, masterCC) + h.Write([]byte(walletID)) + walletCC := h.Sum(nil) + + return c.derivingPubkeyFromPath(masterPub, walletCC, path, curve) } // derivingPubkeyFromPath performs the actual derivation. diff --git a/pkg/mpc/ecdsa_signing_session.go b/pkg/mpc/ecdsa_signing_session.go index 75b3f27..bbcc7ea 100644 --- a/pkg/mpc/ecdsa_signing_session.go +++ b/pkg/mpc/ecdsa_signing_session.go @@ -138,7 +138,7 @@ func (s *ecdsaSigningSession) Init(tx *big.Int) error { if len(s.derivationPath) > 0 { logger.Info("Deriving key from derivation path", "derivationPath", s.derivationPath) - il, extendedChildPk, errorDerivation := s.ckd.Derive(data.ECDSAPub, s.derivationPath, tss.S256()) + il, extendedChildPk, errorDerivation := s.ckd.Derive(s.walletID, data.ECDSAPub, s.derivationPath, tss.S256()) if errorDerivation != nil { return errors.Wrap(errorDerivation, "Failed to derive key") } diff --git a/pkg/mpc/eddsa_signing_session.go b/pkg/mpc/eddsa_signing_session.go index 8921c1f..d47c9ff 100644 --- a/pkg/mpc/eddsa_signing_session.go +++ b/pkg/mpc/eddsa_signing_session.go @@ -127,7 +127,7 @@ func (s *eddsaSigningSession) Init(tx *big.Int) error { if len(s.derivationPath) > 0 { logger.Info("Deriving key from derivation path", "derivationPath", s.derivationPath) - il, extendedChildPk, errorDerivation := s.ckd.Derive(data.EDDSAPub, s.derivationPath, tss.Edwards()) + il, extendedChildPk, errorDerivation := s.ckd.Derive(s.walletID, data.EDDSAPub, s.derivationPath, tss.Edwards()) if errorDerivation != nil { return errors.Wrap(errorDerivation, "Failed to derive key") } From c2468a0e142f00b3fc71388d82afe13e0058125f Mon Sep 17 00:00:00 2001 From: vietddude Date: Fri, 31 Oct 2025 15:49:16 +0700 Subject: [PATCH 07/21] Add chain_code setup instructions and integrate chain_code into configuration --- INSTALLATION.md | 23 +++++++++++++++++++++++ README.md | 12 ++++++++++++ cmd/mpcium/main.go | 11 ++++++++++- pkg/config/init.go | 1 + pkg/mpc/ckd.go | 25 +++++++++++++++++-------- setup_identities.sh | 30 ++++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+), 9 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index a0754d2..212a458 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -56,6 +56,29 @@ Detailed steps can be found in [SETUP.md](SETUP.md). --- +## chain_code setup (required) + +Generate one 32-byte hex chain code and set it in all configs: + +```bash +cd /home/carmy/Documents/works/mpcium +CC=$(openssl rand -hex 32) && echo "$CC" > .chain_code +sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" config.yaml +for n in node0 node1 node2; do + sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" "$n/config.yaml" +done +``` + +Start nodes normally (no env export needed): + +```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..5a27ea6 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,18 @@ 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) +- Mpcium derives child keys using a master chain code. +- Provide a single 32-byte hex value in `config.yaml` under `chain_code`, and use the same value for all nodes. +- Example to generate once and set: +```bash +CC=$(openssl rand -hex 32) +sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" config.yaml +for n in node0 node1 node2; do + sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" "$n/config.yaml" +done +``` + ## 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 be851be..3c2c1a0 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -203,7 +203,8 @@ func runNode(ctx context.Context, c *cli.Command) error { peerNodeIDs := GetPeerIDs(peers) peerRegistry := mpc.NewRegistry(nodeID, peerNodeIDs, consulClient.KV(), directMessaging, pubsub, identityStore) - ckd, err := mpc.NewCKD() + chainCodeHex := viper.GetString("chain_code") + ckd, err := mpc.NewCKDFromHex(chainCodeHex) if err != nil { logger.Fatal("Failed to create ckd store", err) } @@ -449,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/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/mpc/ckd.go b/pkg/mpc/ckd.go index 7d24a95..4ac437a 100644 --- a/pkg/mpc/ckd.go +++ b/pkg/mpc/ckd.go @@ -33,26 +33,35 @@ type CKD struct { mu sync.RWMutex } -// NewCKD loads chain code from environment variable CHAIN_CODE (hex-encoded). -func NewCKD() (*CKD, error) { - envVal := os.Getenv("CHAIN_CODE") - if envVal == "" { - return nil, fmt.Errorf("CHAIN_CODE not set in environment") +// 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(envVal) + code, err := hex.DecodeString(hexStr) if err != nil { - return nil, fmt.Errorf("invalid CHAIN_CODE hex: %w", err) + 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) } - logger.Info("Loaded static chain code from environment") + logger.Info("Loaded static chain code from config") return &CKD{masterChainCode: code}, nil } +// NewCKD loads chain code from environment variable CHAIN_CODE (hex-encoded). +// Deprecated: prefer NewCKDFromHex with config-provided value. +func NewCKD() (*CKD, error) { + envVal := os.Getenv("CHAIN_CODE") + if envVal == "" { + return nil, fmt.Errorf("CHAIN_CODE not set in environment") + } + return NewCKDFromHex(envVal) +} + // GetMasterChainCode returns a copy of the chain code. func (c *CKD) GetMasterChainCode() []byte { c.mu.RLock() diff --git a/setup_identities.sh b/setup_identities.sh index 3b3cd07..ab145f9 100755 --- a/setup_identities.sh +++ b/setup_identities.sh @@ -30,6 +30,36 @@ 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 identity files to all nodes echo "šŸ”„ Distributing identity files across nodes..." for i in $(seq 0 $((NUM_NODES-1))); do From 4bc9344cf3c85d6c7ee9de279a016ea70f025e2c Mon Sep 17 00:00:00 2001 From: vietddude Date: Fri, 31 Oct 2025 15:56:01 +0700 Subject: [PATCH 08/21] Enhance setup scripts: distribute event initiator public key to node configs and add build step in setup.sh --- .gitignore | 1 + setup.sh | 2 ++ setup_identities.sh | 17 +++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/.gitignore b/.gitignore index 21e6163..90d1883 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ node2 config.yaml .vscode .vagrant +.chain_code \ No newline at end of file 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 ab145f9..2562ae4 100755 --- a/setup_identities.sh +++ b/setup_identities.sh @@ -60,6 +60,23 @@ for i in $(seq 0 $((NUM_NODES-1))); do 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 From 887ebe281d9811b51fa9f3cec7f245fa4661c6f0 Mon Sep 17 00:00:00 2001 From: vietddude Date: Tue, 4 Nov 2025 11:43:24 +0700 Subject: [PATCH 09/21] Update dependencies: replace bnb-chain/tss-lib with fystack/tss-lib and add btcec v2.3.2 --- e2e/go.mod | 2 ++ e2e/go.sum | 4 ++-- go.mod | 4 ++-- go.sum | 8 +++----- 4 files changed, 9 insertions(+), 9 deletions(-) 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/go.mod b/go.mod index c515c5d..24de194 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( 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/decred/dcrd/dcrec/edwards/v2 v2.0.3 github.com/dgraph-io/badger/v4 v4.7.0 github.com/google/uuid v1.6.0 @@ -41,7 +42,6 @@ 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/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 @@ -107,4 +107,4 @@ require ( 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/vietddude/tss-lib/v2 v2.0.1 +replace github.com/bnb-chain/tss-lib/v2 => github.com/fystack/tss-lib/v2 v2.0.1 diff --git a/go.sum b/go.sum index a3dfd2a..4bc0231 100644 --- a/go.sum +++ b/go.sum @@ -116,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= @@ -221,7 +223,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -364,8 +365,6 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o= github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= -github.com/vietddude/tss-lib/v2 v2.0.1 h1:mP8hXAxgjvZk3hgNulDigg4ws8OG0X2SvS0VcLk6EAk= -github.com/vietddude/tss-lib/v2 v2.0.1/go.mod h1:CaMK1w/NovT3qXfoa90o4Sc50yjjQW8r5BgVe9FOMMw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -541,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= From c10ecc40991e37f4fb2c6064e8a307797ce13ac2 Mon Sep 17 00:00:00 2001 From: vietddude Date: Tue, 4 Nov 2025 11:44:02 +0700 Subject: [PATCH 10/21] Fix reshare event handling: ensure successEvent.PubKey is non-empty before send result --- pkg/eventconsumer/event_consumer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/eventconsumer/event_consumer.go b/pkg/eventconsumer/event_consumer.go index c010307..8dd9e6a 100644 --- a/pkg/eventconsumer/event_consumer.go +++ b/pkg/eventconsumer/event_consumer.go @@ -728,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) From 831eb00ce484746349f2b54a790f1338ed110e07 Mon Sep 17 00:00:00 2001 From: vietddude Date: Tue, 4 Nov 2025 11:54:01 +0700 Subject: [PATCH 11/21] Add chain_code config and integration CKD sign into e2e --- e2e/config.test.yaml.template | 1 + e2e/setup_test_identities.sh | 23 ++- e2e/sign_ckd_test.go | 338 ++++++++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 e2e/sign_ckd_test.go diff --git a/e2e/config.test.yaml.template b/e2e/config.test.yaml.template index 8d37279..dfe434a 100644 --- a/e2e/config.test.yaml.template +++ b/e2e/config.test.yaml.template @@ -11,3 +11,4 @@ nats: max_concurrent_keygen: 1 max_concurrent_signing: 10 session_warm_up_delay_ms: 500 +chain_code: "{{.CKDChainCode}}" 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 + } + } + } +} From 5d2c55a26e132d42814c81bcb20696f9e0c5529b Mon Sep 17 00:00:00 2001 From: vietddude Date: Tue, 4 Nov 2025 13:21:00 +0700 Subject: [PATCH 12/21] Update e2e test matrix to include TestCKDSigning --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 81294778458cd2010e023307cf3da8af84a2f343 Mon Sep 17 00:00:00 2001 From: anhthii Date: Wed, 26 Nov 2025 21:43:20 +0700 Subject: [PATCH 13/21] Remove unused functions --- pkg/mpc/ckd.go | 16 ---------------- pkg/mpc/node.go | 9 --------- 2 files changed, 25 deletions(-) diff --git a/pkg/mpc/ckd.go b/pkg/mpc/ckd.go index 4ac437a..7d53dd0 100644 --- a/pkg/mpc/ckd.go +++ b/pkg/mpc/ckd.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "math/big" - "os" "sync" "github.com/bnb-chain/tss-lib/v2/crypto" @@ -16,7 +15,6 @@ import ( 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" - "github.com/fystack/mpcium/pkg/logger" ) const chainCodeLength = 32 @@ -38,7 +36,6 @@ 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) @@ -46,22 +43,9 @@ func NewCKDFromHex(hexStr string) (*CKD, error) { if len(code) != chainCodeLength { return nil, fmt.Errorf("%w: got %d, want %d", ErrInvalidChainCode, len(code), chainCodeLength) } - - logger.Info("Loaded static chain code from config") - return &CKD{masterChainCode: code}, nil } -// NewCKD loads chain code from environment variable CHAIN_CODE (hex-encoded). -// Deprecated: prefer NewCKDFromHex with config-provided value. -func NewCKD() (*CKD, error) { - envVal := os.Getenv("CHAIN_CODE") - if envVal == "" { - return nil, fmt.Errorf("CHAIN_CODE not set in environment") - } - return NewCKDFromHex(envVal) -} - // GetMasterChainCode returns a copy of the chain code. func (c *CKD) GetMasterChainCode() []byte { c.mu.RLock() diff --git a/pkg/mpc/node.go b/pkg/mpc/node.go index 6e8a7e3..e03bd01 100644 --- a/pkg/mpc/node.go +++ b/pkg/mpc/node.go @@ -1,7 +1,6 @@ package mpc import ( - "bytes" "encoding/json" "fmt" "slices" @@ -42,14 +41,6 @@ type Node struct { ckd *CKD } -func PartyIDToRoutingDest(partyID *tss.PartyID) string { - return string(partyID.KeyInt().Bytes()) -} - -func ComparePartyIDs(x, y *tss.PartyID) bool { - return bytes.Equal(x.KeyInt().Bytes(), y.KeyInt().Bytes()) -} - func NewNode( nodeID string, peerIDs []string, From 4d997bb0cf1da2125ba6ba2e4677566d5b451c46 Mon Sep 17 00:00:00 2001 From: anhthii Date: Tue, 2 Dec 2025 13:04:54 +0700 Subject: [PATCH 14/21] Update derivatoin logic --- pkg/mpc/ckd.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pkg/mpc/ckd.go b/pkg/mpc/ckd.go index 7d53dd0..2b452b8 100644 --- a/pkg/mpc/ckd.go +++ b/pkg/mpc/ckd.go @@ -2,13 +2,10 @@ package mpc import ( "crypto/elliptic" - "crypto/hmac" - "crypto/sha512" "encoding/hex" "errors" "fmt" "math/big" - "sync" "github.com/bnb-chain/tss-lib/v2/crypto" "github.com/bnb-chain/tss-lib/v2/crypto/ckd" @@ -28,7 +25,6 @@ var ( // CKD handles Child Key Derivation (ENV-based) type CKD struct { masterChainCode []byte - mu sync.RWMutex } // NewCKDFromHex creates CKD from a hex-encoded chain code string (32 bytes). @@ -48,14 +44,14 @@ func NewCKDFromHex(hexStr string) (*CKD, error) { // GetMasterChainCode returns a copy of the chain code. func (c *CKD) GetMasterChainCode() []byte { - c.mu.RLock() - defer c.mu.RUnlock() 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 @@ -64,15 +60,9 @@ func (c *CKD) Derive(walletID string, masterPub *crypto.ECPoint, path []uint32, return nil, nil, errors.New("curve cannot be nil") } - c.mu.RLock() masterCC := append([]byte(nil), c.masterChainCode...) - c.mu.RUnlock() - h := hmac.New(sha512.New, masterCC) - h.Write([]byte(walletID)) - walletCC := h.Sum(nil) - - return c.derivingPubkeyFromPath(masterPub, walletCC, path, curve) + return c.derivingPubkeyFromPath(masterPub, masterCC, path, curve) } // derivingPubkeyFromPath performs the actual derivation. From 5c90a5854c8bb88917bbb72e18caaef963b150ab Mon Sep 17 00:00:00 2001 From: anhthii Date: Tue, 2 Dec 2025 13:06:56 +0700 Subject: [PATCH 15/21] Add hd wallet examples for both ecdsa and eddsa --- examples/hdwallet/ecdsa/main.go | 434 +++++++++++++++++++++++++++ examples/hdwallet/eddsa/main.go | 417 +++++++++++++++++++++++++ go.mod | 4 +- go.sum | 4 +- pkg/ckdutil/child_derivation.go | 183 +++++++++++ pkg/ckdutil/child_derivation_test.go | 79 +++++ pkg/mpc/ecdsa_signing_session.go | 5 +- pkg/mpc/eddsa_signing_session.go | 5 +- 8 files changed, 1121 insertions(+), 10 deletions(-) create mode 100644 examples/hdwallet/ecdsa/main.go create mode 100644 examples/hdwallet/eddsa/main.go create mode 100644 pkg/ckdutil/child_derivation.go create mode 100644 pkg/ckdutil/child_derivation_test.go diff --git a/examples/hdwallet/ecdsa/main.go b/examples/hdwallet/ecdsa/main.go new file mode 100644 index 0000000..4a99a8a --- /dev/null +++ b/examples/hdwallet/ecdsa/main.go @@ -0,0 +1,434 @@ +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 +) + +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, 2) + for i := 0; i < 2; i++ { + childIndex := uint32(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..b103365 --- /dev/null +++ b/examples/hdwallet/eddsa/main.go @@ -0,0 +1,417 @@ +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 = 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 := 0; i < addressCount; i++ { + childIndex := uint32(i) + path := []uint32{solPurpose, solCoinType, childIndex, 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: 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\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 24de194..098e353 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( 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 @@ -43,7 +44,6 @@ require ( 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/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 @@ -107,4 +107,4 @@ require ( 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 +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 4bc0231..697a3ea 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +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/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= 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/mpc/ecdsa_signing_session.go b/pkg/mpc/ecdsa_signing_session.go index bbcc7ea..60dec8a 100644 --- a/pkg/mpc/ecdsa_signing_session.go +++ b/pkg/mpc/ecdsa_signing_session.go @@ -137,15 +137,14 @@ func (s *ecdsaSigningSession) Init(tx *big.Int) error { } if len(s.derivationPath) > 0 { - logger.Info("Deriving key from derivation path", "derivationPath", s.derivationPath) il, extendedChildPk, errorDerivation := s.ckd.Derive(s.walletID, data.ECDSAPub, s.derivationPath, tss.S256()) if errorDerivation != nil { - return errors.Wrap(errorDerivation, "Failed to derive key") + 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, "Failed to update public key") + 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) diff --git a/pkg/mpc/eddsa_signing_session.go b/pkg/mpc/eddsa_signing_session.go index d47c9ff..f4df093 100644 --- a/pkg/mpc/eddsa_signing_session.go +++ b/pkg/mpc/eddsa_signing_session.go @@ -126,15 +126,14 @@ func (s *eddsaSigningSession) Init(tx *big.Int) error { } if len(s.derivationPath) > 0 { - logger.Info("Deriving key from derivation path", "derivationPath", s.derivationPath) il, extendedChildPk, errorDerivation := s.ckd.Derive(s.walletID, data.EDDSAPub, s.derivationPath, tss.Edwards()) if errorDerivation != nil { - return errors.Wrap(errorDerivation, "Failed to derive key") + 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, "Failed to update public key") + 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) From d6684ab6d875fba395e086aa0c4d6634bcc8e3d8 Mon Sep 17 00:00:00 2001 From: anhthii Date: Tue, 2 Dec 2025 13:11:22 +0700 Subject: [PATCH 16/21] Fix lint issue --- examples/hdwallet/ecdsa/main.go | 9 ++++++--- examples/hdwallet/eddsa/main.go | 9 ++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/hdwallet/ecdsa/main.go b/examples/hdwallet/ecdsa/main.go index 4a99a8a..f73774f 100644 --- a/examples/hdwallet/ecdsa/main.go +++ b/examples/hdwallet/ecdsa/main.go @@ -30,6 +30,9 @@ const ( 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 { @@ -151,9 +154,9 @@ func main() { logger.Fatal("chain_code not found in config", fmt.Errorf("required for HD derivation")) } - addresses := make([]*DerivedAddress, 2) - for i := 0; i < 2; i++ { - childIndex := uint32(i) + 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!) diff --git a/examples/hdwallet/eddsa/main.go b/examples/hdwallet/eddsa/main.go index b103365..2654cf7 100644 --- a/examples/hdwallet/eddsa/main.go +++ b/examples/hdwallet/eddsa/main.go @@ -35,7 +35,7 @@ const ( solChange = 0 // External chain // Number of addresses to derive; change this to derive more or fewer addresses. - addressCount = 2 + addressCount = uint32(2) ) type DerivedAddress struct { @@ -158,9 +158,8 @@ func main() { } addresses := make([]*DerivedAddress, addressCount) - for i := 0; i < addressCount; i++ { - childIndex := uint32(i) - path := []uint32{solPurpose, solCoinType, childIndex, solChange} + 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) @@ -180,7 +179,7 @@ func main() { address := deriveSolanaAddress(childPubKey) addresses[i] = &DerivedAddress{ - Index: childIndex, + Index: i, DerivationPath: path, PublicKey: childPubKey, Address: address, From 86474eb12aa18bd09ac7634f3cd491067a38f4a7 Mon Sep 17 00:00:00 2001 From: anhthii Date: Tue, 9 Dec 2025 12:52:59 +0700 Subject: [PATCH 17/21] Update cI --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++------ go.mod | 4 +--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d090edb..4575baa 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: @@ -101,8 +101,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 +125,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 +169,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 +180,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 +255,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/go.mod b/go.mod index 098e353..3e305f5 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.5 require ( filippo.io/age v1.2.1 From fc853a05f39690e29e73a0c3be02f86d15e62496 Mon Sep 17 00:00:00 2001 From: anhthii Date: Tue, 9 Dec 2025 12:54:58 +0700 Subject: [PATCH 18/21] Udpate chain code instruction --- INSTALLATION.md | 35 +++++++++++++++++++++++++---- README.md | 17 +++++--------- config.prod.yaml.template | 9 +++++++- config.yaml.template | 7 ++++++ deployments/systemd/setup-config.sh | 21 +++++++++++++++++ e2e/config.test.yaml.template | 3 +++ 6 files changed, 76 insertions(+), 16 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index 212a458..f41d0cf 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -56,20 +56,47 @@ Detailed steps can be found in [SETUP.md](SETUP.md). --- -## chain_code setup (required) +## chain_code setup (REQUIRED) -Generate one 32-byte hex chain code and set it in all configs: +### 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 -cd /home/carmy/Documents/works/mpcium +# 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 (no env export needed): +Start nodes normally: ```bash cd node0 && mpcium start -n node0 diff --git a/README.md b/README.md index 5a27ea6..5fa615b 100644 --- a/README.md +++ b/README.md @@ -133,17 +133,12 @@ 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) -- Mpcium derives child keys using a master chain code. -- Provide a single 32-byte hex value in `config.yaml` under `chain_code`, and use the same value for all nodes. -- Example to generate once and set: -```bash -CC=$(openssl rand -hex 32) -sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" config.yaml -for n in node0 node1 node2; do - sed -i -E "s|^([[:space:]]*chain_code:).*|\1 \"$CC\"|" "$n/config.yaml" -done -``` +#### 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 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 dfe434a..910ae6c 100644 --- a/e2e/config.test.yaml.template +++ b/e2e/config.test.yaml.template @@ -11,4 +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}}" From 7abc4ad3a899f23fc20908fe0d8df7cdf6ef3376 Mon Sep 17 00:00:00 2001 From: anhthii Date: Tue, 9 Dec 2025 14:03:57 +0700 Subject: [PATCH 19/21] Update go version --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 3e305f5..cf8dc21 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fystack/mpcium -go 1.25.5 +go 1.25.0 require ( filippo.io/age v1.2.1 From ed0dfc73f0de2b88a1db736ba99ad598f0218730 Mon Sep 17 00:00:00 2001 From: anhthii Date: Tue, 9 Dec 2025 14:25:51 +0700 Subject: [PATCH 20/21] Udpate golint --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4575baa..21a72fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 From 69250f87e0f51fa1d245acba4871475c9179f632 Mon Sep 17 00:00:00 2001 From: anhthii Date: Tue, 9 Dec 2025 14:49:52 +0700 Subject: [PATCH 21/21] Update installation instruction --- INSTALLATION.md | 42 +++++++++++++++++++++++++++++++++++++----- setup_identities.sh | 12 +++++++++++- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index f41d0cf..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 -chmod +x ./setup.sh -./setup.sh +./setup_initiator.sh ``` -Detailed steps can be found in [SETUP.md](SETUP.md). +This generates the event initiator identity used to authorize MPC operations. + +#### 3. Set up Node Identities + +```bash +./setup_identities.sh +``` + +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. --- @@ -63,6 +93,7 @@ Detailed steps can be found in [SETUP.md](SETUP.md). 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 @@ -92,6 +123,7 @@ echo "Chain code configured: $CC" ``` **Example config.yaml entry:** + ```yaml chain_code: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" ``` diff --git a/setup_identities.sh b/setup_identities.sh index 2562ae4..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..."