diff --git a/README.md b/README.md index 5f958f4..0c8c468 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ import ( "github.com/data-preservation-programs/go-synapse/constants" "github.com/data-preservation-programs/go-synapse/pdp" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" @@ -83,10 +84,10 @@ func main() { privateKey, _ := crypto.HexToECDSA("your-private-key-hex") client, _ := ethclient.Dial("https://api.calibration.node.glif.io/rpc/v1") - signer := pdp.NewPrivateKeySigner(privateKey) + s, _ := signer.NewSecp256k1SignerFromECDSA(privateKey) // Create proof set manager (recommended: use NewManagerWithContext) - manager, err := pdp.NewManagerWithContext(ctx, client, signer, constants.NetworkCalibration) + manager, err := pdp.NewManagerWithContext(ctx, client, s, constants.NetworkCalibration) if err != nil { log.Fatal(err) } @@ -94,7 +95,7 @@ func main() { // For custom gas buffer configuration: // config := pdp.DefaultManagerConfig() // config.GasBufferPercent = 15 // Custom 15% buffer instead of default 10% - // manager, err := pdp.NewManagerWithConfig(ctx, client, signer, constants.NetworkCalibration, &config) + // manager, err := pdp.NewManagerWithConfig(ctx, client, s, constants.NetworkCalibration, &config) // Create a new proof set result, err := manager.CreateProofSet(ctx, pdp.CreateProofSetOptions{ diff --git a/examples/create-proof-set/main.go b/examples/create-proof-set/main.go index c86acb2..a516ab5 100644 --- a/examples/create-proof-set/main.go +++ b/examples/create-proof-set/main.go @@ -9,6 +9,7 @@ import ( "github.com/data-preservation-programs/go-synapse/constants" "github.com/data-preservation-programs/go-synapse/pdp" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" @@ -72,9 +73,12 @@ func run() error { log.Printf("Connected to %s (Chain ID: %d)", network, chainID.Int64()) - // Create proof set manager - signer := pdp.NewPrivateKeySigner(privateKey) - manager, err := pdp.NewManagerWithContext(ctx, client, signer, network) + // Create signer and proof set manager + s, err := signer.NewSecp256k1SignerFromECDSA(privateKey) + if err != nil { + return fmt.Errorf("failed to create signer: %w", err) + } + manager, err := pdp.NewManagerWithContext(ctx, client, s, network) if err != nil { return fmt.Errorf("failed to create manager: %w", err) } @@ -82,7 +86,7 @@ func run() error { // Alternative: Use NewManagerWithConfig for custom gas buffer // config := pdp.DefaultManagerConfig() // config.GasBufferPercent = 15 // Custom 15% buffer - // manager, err := pdp.NewManagerWithConfig(ctx, client, signer, network, &config) + // manager, err := pdp.NewManagerWithConfig(ctx, client, s, network, &config) address := crypto.PubkeyToAddress(privateKey.PublicKey) log.Printf("Using address: %s", address.Hex()) diff --git a/go.mod b/go.mod index 4110876..d56bfd2 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,14 @@ module github.com/data-preservation-programs/go-synapse go 1.22 require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 github.com/ethereum/go-ethereum v1.14.12 + github.com/filecoin-project/go-address v1.1.0 github.com/filecoin-project/go-commp-utils/v2 v2.1.0 + github.com/filecoin-project/go-state-types v0.14.0 github.com/ipfs/go-cid v0.4.1 + github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 + github.com/supranational/blst v0.3.16 ) require ( @@ -17,14 +22,11 @@ require ( github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect - github.com/filecoin-project/go-address v1.1.0 // indirect github.com/filecoin-project/go-fil-commcid v0.1.0 // indirect github.com/filecoin-project/go-fil-commp-hashhash v0.2.0 // indirect github.com/filecoin-project/go-padreader v0.0.1 // indirect - github.com/filecoin-project/go-state-types v0.14.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect @@ -35,7 +37,6 @@ require ( github.com/ipfs/go-ipld-cbor v0.1.0 // indirect github.com/ipfs/go-ipld-format v0.6.0 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect @@ -47,7 +48,6 @@ require ( github.com/polydawn/refmt v0.89.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/supranational/blst v0.3.13 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/whyrusleeping/cbor-gen v0.1.2 // indirect diff --git a/go.sum b/go.sum index c102885..cf24d89 100644 --- a/go.sum +++ b/go.sum @@ -215,8 +215,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= -github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= diff --git a/integration_test.go b/integration_test.go index a23c872..d1fd821 100644 --- a/integration_test.go +++ b/integration_test.go @@ -11,6 +11,7 @@ import ( "github.com/data-preservation-programs/go-synapse/constants" "github.com/data-preservation-programs/go-synapse/pdp" + "github.com/data-preservation-programs/go-synapse/signer" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" @@ -68,8 +69,11 @@ func TestIntegration_ProofSetLifecycle(t *testing.T) { defer client.Close() // Create proof set manager - signer := pdp.NewPrivateKeySigner(privateKey) - manager, err := pdp.NewManagerWithContext(ctx, client, signer, constants.NetworkCalibration) + s, err := signer.NewSecp256k1SignerFromECDSA(privateKey) + if err != nil { + t.Fatalf("Failed to create signer: %v", err) + } + manager, err := pdp.NewManagerWithContext(ctx, client, s, constants.NetworkCalibration) if err != nil { t.Fatalf("Failed to create manager: %v", err) } diff --git a/pdp/manager.go b/pdp/manager.go index becee40..f164d52 100644 --- a/pdp/manager.go +++ b/pdp/manager.go @@ -141,7 +141,7 @@ func NewManagerWithConfig(ctx context.Context, client *ethclient.Client, signer return nil, fmt.Errorf("failed to create contract instance: %w", err) } - address := signer.Address() + address := signer.EVMAddress() nonceManager := txutil.NewNonceManager(client, address) return &Manager{ @@ -157,17 +157,13 @@ func NewManagerWithConfig(ctx context.Context, client *ethclient.Client, signer } func (m *Manager) newTransactor(ctx context.Context, nonce uint64, value *big.Int) (*bind.TransactOpts, error) { - signerFn, err := m.signer.SignerFunc(m.chainID) + auth, err := m.signer.Transactor(m.chainID) if err != nil { - return nil, fmt.Errorf("failed to create signer: %w", err) + return nil, fmt.Errorf("failed to create transactor: %w", err) } - auth := &bind.TransactOpts{ - From: m.address, - Signer: signerFn, - Nonce: big.NewInt(int64(nonce)), - Context: ctx, - } + auth.Nonce = big.NewInt(int64(nonce)) + auth.Context = ctx if value != nil { auth.Value = value } diff --git a/pdp/signer.go b/pdp/signer.go index 61c10ed..c04ab3b 100644 --- a/pdp/signer.go +++ b/pdp/signer.go @@ -2,44 +2,24 @@ package pdp import ( "crypto/ecdsa" - "fmt" - "math/big" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" + "github.com/data-preservation-programs/go-synapse/signer" ) -// Signer provides transaction signing for the Manager without exposing key material. -type Signer interface { - Address() common.Address - SignerFunc(chainID *big.Int) (bind.SignerFn, error) -} +// Signer is kept for backward compatibility. New code should use signer.EVMSigner. +type Signer = signer.EVMSigner -// PrivateKeySigner is a simple signer backed by a local ECDSA private key. -type PrivateKeySigner struct { - privateKey *ecdsa.PrivateKey - address common.Address -} +// PrivateKeySigner is kept for backward compatibility. +type PrivateKeySigner = signer.Secp256k1Signer -// NewPrivateKeySigner creates a signer backed by the provided private key. +// NewPrivateKeySigner creates a dual-protocol signer from an ECDSA private key. +// Kept for backward compatibility — new code should use +// signer.NewSecp256k1SignerFromECDSA or signer.NewSecp256k1Signer. func NewPrivateKeySigner(privateKey *ecdsa.PrivateKey) *PrivateKeySigner { - return &PrivateKeySigner{ - privateKey: privateKey, - address: crypto.PubkeyToAddress(privateKey.PublicKey), - } -} - -// Address returns the signer address. -func (s *PrivateKeySigner) Address() common.Address { - return s.address -} - -// SignerFunc returns a bind.SignerFn using the provided chain ID. -func (s *PrivateKeySigner) SignerFunc(chainID *big.Int) (bind.SignerFn, error) { - auth, err := bind.NewKeyedTransactorWithChainID(s.privateKey, chainID) + s, err := signer.NewSecp256k1SignerFromECDSA(privateKey) if err != nil { - return nil, fmt.Errorf("failed to create transactor: %w", err) + // should never happen for a valid *ecdsa.PrivateKey + panic("NewPrivateKeySigner: " + err.Error()) } - return auth.Signer, nil + return s } diff --git a/signer/bls.go b/signer/bls.go new file mode 100644 index 0000000..ce5ed6b --- /dev/null +++ b/signer/bls.go @@ -0,0 +1,61 @@ +package signer + +import ( + "fmt" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/crypto" + blst "github.com/supranational/blst/bindings/go" +) + +// BLSSigner implements Signer (but not EVMSigner) backed by a BLS private key. +type BLSSigner struct { + raw []byte + sk *blst.SecretKey + filAddr address.Address +} + +// NewBLSSigner creates a Filecoin-only signer from raw BLS key bytes. +func NewBLSSigner(raw []byte) (*BLSSigner, error) { + sk := new(blst.SecretKey).Deserialize(raw) + if sk == nil { + return nil, fmt.Errorf("invalid BLS secret key") + } + + pk := new(blst.P1Affine).From(sk).Compress() + filAddr, err := address.NewBLSAddress(pk) + if err != nil { + return nil, fmt.Errorf("deriving BLS address: %w", err) + } + + return &BLSSigner{ + raw: raw, + sk: sk, + filAddr: filAddr, + }, nil +} + +// NewBLSSignerFromLotusExport creates a signer from a lotus-exported BLS key. +func NewBLSSignerFromLotusExport(exported string) (*BLSSigner, error) { + ki, err := decodeLotusKey(exported) + if err != nil { + return nil, err + } + if ki.Type != "bls" { + return nil, fmt.Errorf("expected bls key, got %s", ki.Type) + } + return NewBLSSigner(ki.PrivateKey) +} + +func (s *BLSSigner) FilecoinAddress() address.Address { + return s.filAddr +} + +// Sign produces a BLS signature over the raw message bytes (no prehash). +func (s *BLSSigner) Sign(msg []byte) (*crypto.Signature, error) { + sig := new(blst.P2Affine).Sign(s.sk, msg, []byte("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_")) + return &crypto.Signature{ + Type: crypto.SigTypeBLS, + Data: sig.Compress(), + }, nil +} diff --git a/signer/lotus.go b/signer/lotus.go new file mode 100644 index 0000000..e34b193 --- /dev/null +++ b/signer/lotus.go @@ -0,0 +1,28 @@ +package signer + +import "fmt" + +// FromLotusExport creates a Signer from a lotus-exported private key string. +// The key type (secp256k1 or bls) is detected automatically. +// For secp256k1 keys, the returned Signer also implements EVMSigner. +func FromLotusExport(exported string) (Signer, error) { + ki, err := decodeLotusKey(exported) + if err != nil { + return nil, err + } + switch ki.Type { + case "secp256k1": + return NewSecp256k1Signer(ki.PrivateKey) + case "bls": + return NewBLSSigner(ki.PrivateKey) + default: + return nil, fmt.Errorf("unsupported key type: %s", ki.Type) + } +} + +// AsEVM checks whether a Signer can sign EVM transactions. +// Returns nil, false for BLS keys. +func AsEVM(s Signer) (EVMSigner, bool) { + e, ok := s.(EVMSigner) + return e, ok +} diff --git a/signer/secp256k1.go b/signer/secp256k1.go new file mode 100644 index 0000000..1d0af32 --- /dev/null +++ b/signer/secp256k1.go @@ -0,0 +1,139 @@ +package signer + +import ( + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/crypto" + + blake2b "github.com/minio/blake2b-simd" + + dcrdecdsa "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" + dcrdsecp "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +// Secp256k1Signer implements EVMSigner backed by a secp256k1 private key. +// It can sign both Filecoin messages and Ethereum transactions. +type Secp256k1Signer struct { + raw []byte // raw 32-byte scalar + ecdsaKey *ecdsa.PrivateKey + filAddr address.Address + ethAddr common.Address +} + +// NewSecp256k1Signer creates a dual-protocol signer from raw key bytes. +// The input is left-padded to 32 bytes if shorter (e.g. from big.Int.Bytes()). +func NewSecp256k1Signer(raw []byte) (*Secp256k1Signer, error) { + if len(raw) == 0 || len(raw) > 32 { + return nil, fmt.Errorf("invalid key length: %d", len(raw)) + } + + // left-pad to 32 bytes — big.Int.Bytes() drops leading zeros + var padded [32]byte + copy(padded[32-len(raw):], raw) + + ecdsaKey, err := ethcrypto.ToECDSA(padded[:]) + if err != nil { + return nil, fmt.Errorf("invalid secp256k1 key: %w", err) + } + + return newFromECDSA(ecdsaKey, padded[:]) +} + +// NewSecp256k1SignerFromECDSA creates a dual-protocol signer from a go-ethereum +// ECDSA private key. This is the preferred constructor when you already have +// a parsed key (e.g. from crypto.GenerateKey or crypto.HexToECDSA). +func NewSecp256k1SignerFromECDSA(key *ecdsa.PrivateKey) (*Secp256k1Signer, error) { + if key == nil { + return nil, fmt.Errorf("nil private key") + } + raw := ethcrypto.FromECDSA(key) // always 32 bytes, zero-padded + return newFromECDSA(key, raw) +} + +func newFromECDSA(ecdsaKey *ecdsa.PrivateKey, raw []byte) (*Secp256k1Signer, error) { + dcrdKey := dcrdsecp.PrivKeyFromBytes(raw) + uncompressed := dcrdKey.PubKey().SerializeUncompressed() + filAddr, err := address.NewSecp256k1Address(uncompressed) + if err != nil { + return nil, fmt.Errorf("deriving filecoin address: %w", err) + } + + ethAddr := ethcrypto.PubkeyToAddress(ecdsaKey.PublicKey) + + return &Secp256k1Signer{ + raw: raw, + ecdsaKey: ecdsaKey, + filAddr: filAddr, + ethAddr: ethAddr, + }, nil +} + +// NewSecp256k1SignerFromLotusExport creates a signer from a lotus-exported +// private key (hex-encoded JSON with Type and PrivateKey fields). +// This is the format produced by `lotus wallet export`. +func NewSecp256k1SignerFromLotusExport(exported string) (*Secp256k1Signer, error) { + ki, err := decodeLotusKey(exported) + if err != nil { + return nil, err + } + if ki.Type != "secp256k1" { + return nil, fmt.Errorf("expected secp256k1 key, got %s", ki.Type) + } + return NewSecp256k1Signer(ki.PrivateKey) +} + +func (s *Secp256k1Signer) FilecoinAddress() address.Address { + return s.filAddr +} + +// Sign produces a Filecoin-native signature (blake2b-256 hash, R|S|V format). +func (s *Secp256k1Signer) Sign(msg []byte) (*crypto.Signature, error) { + hash := blake2b.Sum256(msg) + dcrdKey := dcrdsecp.PrivKeyFromBytes(s.raw) + sig := dcrdecdsa.SignCompact(dcrdKey, hash[:], false) + + // rotate: go from V|R|S to R|S|V, adjust recovery ID + recoveryID := sig[0] + copy(sig, sig[1:]) + sig[64] = recoveryID - 27 + + return &crypto.Signature{ + Type: crypto.SigTypeSecp256k1, + Data: sig, + }, nil +} + +func (s *Secp256k1Signer) EVMAddress() common.Address { + return s.ethAddr +} + +// Transactor returns bind.TransactOpts for signing Ethereum/FEVM transactions. +func (s *Secp256k1Signer) Transactor(chainID *big.Int) (*bind.TransactOpts, error) { + return bind.NewKeyedTransactorWithChainID(s.ecdsaKey, chainID) +} + +// lotusKeyInfo mirrors the JSON structure of a lotus wallet export. +type lotusKeyInfo struct { + Type string `json:"Type"` + PrivateKey []byte `json:"PrivateKey"` +} + +func decodeLotusKey(exported string) (*lotusKeyInfo, error) { + raw, err := hex.DecodeString(exported) + if err != nil { + return nil, fmt.Errorf("decoding hex: %w", err) + } + var ki lotusKeyInfo + if err := json.Unmarshal(raw, &ki); err != nil { + return nil, fmt.Errorf("unmarshaling key: %w", err) + } + return &ki, nil +} diff --git a/signer/signer.go b/signer/signer.go new file mode 100644 index 0000000..6512496 --- /dev/null +++ b/signer/signer.go @@ -0,0 +1,32 @@ +// Package signer provides dual-protocol signing for Filecoin and Ethereum/FEVM. +// +// A secp256k1 private key can sign both native Filecoin messages (blake2b) +// and Ethereum transactions (keccak256). This package exposes both capabilities +// through composable interfaces, eliminating the need for adapter glue in +// every tool that touches both protocols. +// +// BLS keys can only sign Filecoin messages. Attempting to use a BLS key +// for EVM operations returns an error. +package signer + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/crypto" +) + +// Signer signs native Filecoin messages. Every key type can do this. +type Signer interface { + FilecoinAddress() address.Address + Sign(msg []byte) (*crypto.Signature, error) +} + +// EVMSigner signs Ethereum/FEVM transactions. Only secp256k1 keys can do this. +type EVMSigner interface { + Signer + EVMAddress() common.Address + Transactor(chainID *big.Int) (*bind.TransactOpts, error) +} diff --git a/signer/signer_test.go b/signer/signer_test.go new file mode 100644 index 0000000..ff36d8e --- /dev/null +++ b/signer/signer_test.go @@ -0,0 +1,171 @@ +package signer + +import ( + "encoding/hex" + "encoding/json" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/filecoin-project/go-address" +) + +func makeTestLotusExport(keyType string, raw []byte) string { + ki := lotusKeyInfo{Type: keyType, PrivateKey: raw} + j, _ := json.Marshal(ki) + return hex.EncodeToString(j) +} + +func TestSecp256k1Signer_DualProtocol(t *testing.T) { + key, err := ethcrypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + + s, err := NewSecp256k1SignerFromECDSA(key) + if err != nil { + t.Fatal(err) + } + + // filecoin address should be secp256k1 protocol + filAddr := s.FilecoinAddress() + if filAddr.Protocol() != address.SECP256K1 { + t.Errorf("expected secp256k1 address, got protocol %d", filAddr.Protocol()) + } + + // evm address should match go-ethereum derivation + expectedEth := ethcrypto.PubkeyToAddress(key.PublicKey) + if s.EVMAddress() != expectedEth { + t.Errorf("EVMAddress() = %s, want %s", s.EVMAddress(), expectedEth) + } + + // sign a filecoin message + msg := []byte("test message") + sig, err := s.Sign(msg) + if err != nil { + t.Fatal(err) + } + if sig.Type != 1 { // SigTypeSecp256k1 + t.Errorf("signature type = %d, want 1", sig.Type) + } + if len(sig.Data) != 65 { + t.Errorf("signature length = %d, want 65", len(sig.Data)) + } + + // create an evm transactor + opts, err := s.Transactor(big.NewInt(314159)) + if err != nil { + t.Fatal(err) + } + if opts.From != expectedEth { + t.Errorf("Transactor.From = %s, want %s", opts.From, expectedEth) + } +} + +func TestSecp256k1Signer_FromLotusExport(t *testing.T) { + key, err := ethcrypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + exported := makeTestLotusExport("secp256k1", ethcrypto.FromECDSA(key)) + + s, err := NewSecp256k1SignerFromLotusExport(exported) + if err != nil { + t.Fatal(err) + } + + expectedEth := ethcrypto.PubkeyToAddress(key.PublicKey) + if s.EVMAddress() != expectedEth { + t.Errorf("EVMAddress() = %s, want %s", s.EVMAddress(), expectedEth) + } +} + +func TestSecp256k1Signer_RejectsWrongType(t *testing.T) { + exported := makeTestLotusExport("bls", []byte("dummy")) + _, err := NewSecp256k1SignerFromLotusExport(exported) + if err == nil { + t.Error("expected error for bls key, got nil") + } +} + +func TestSecp256k1Signer_PadsShortKeys(t *testing.T) { + // simulate big.Int.Bytes() dropping a leading zero + key, err := ethcrypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + raw := ethcrypto.FromECDSA(key) + + // construct from canonical 32-byte form + s1, err := NewSecp256k1Signer(raw) + if err != nil { + t.Fatal(err) + } + + // construct from potentially short D.Bytes() + s2, err := NewSecp256k1Signer(key.D.Bytes()) + if err != nil { + t.Fatal(err) + } + + if s1.EVMAddress() != s2.EVMAddress() { + t.Errorf("addresses differ: %s vs %s", s1.EVMAddress(), s2.EVMAddress()) + } + if s1.FilecoinAddress() != s2.FilecoinAddress() { + t.Errorf("filecoin addresses differ: %s vs %s", s1.FilecoinAddress(), s2.FilecoinAddress()) + } +} + +func TestFromLotusExport_AutoDetect(t *testing.T) { + key, err := ethcrypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + exported := makeTestLotusExport("secp256k1", ethcrypto.FromECDSA(key)) + + s, err := FromLotusExport(exported) + if err != nil { + t.Fatal(err) + } + + if s.FilecoinAddress().Protocol() != address.SECP256K1 { + t.Error("expected secp256k1 signer") + } + + evm, ok := AsEVM(s) + if !ok { + t.Fatal("secp256k1 signer should satisfy EVMSigner") + } + if evm.EVMAddress() == (common.Address{}) { + t.Error("EVMAddress should not be zero") + } +} + +func TestFromLotusExport_UnsupportedType(t *testing.T) { + exported := makeTestLotusExport("ed25519", []byte("dummy")) + _, err := FromLotusExport(exported) + if err == nil { + t.Error("expected error for unsupported key type") + } +} + +func TestAsEVM(t *testing.T) { + key, err := ethcrypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + + s, err := NewSecp256k1SignerFromECDSA(key) + if err != nil { + t.Fatal(err) + } + + evm, ok := AsEVM(s) + if !ok { + t.Fatal("secp256k1 signer should be EVMSigner") + } + if evm.EVMAddress() == (common.Address{}) { + t.Error("EVMAddress should not be zero") + } +}