Files
mpc/pkg/smart/smart_test.go
T
zeekay e6e3d55e22 feat(hsm): AWS/GCP/Azure KMS password providers, CREATE2 address prediction
- Implement CREATE2 address prediction in PredictAccountAddress() using
  keccak256(0xff ++ factory ++ salt ++ keccak256(initCode))[12:] formula.
  Also add general-purpose PredictCreate2Address() with raw [20]byte params.

- Create pkg/hsm package with PasswordProvider interface and 5 implementations:
  - AWSKMSProvider: decrypts ZAPDB_ENCRYPTED_PASSWORD via AWS KMS Decrypt API
    with full SigV4 signing and EC2 instance metadata credential fallback
  - GCPKMSProvider: decrypts via GCP Cloud KMS REST API with GCE metadata OAuth2
  - AzureKVProvider: unwraps via Azure Key Vault REST API with MSI token auth
  - EnvProvider: reads from LUX_MPC_PASSWORD/ZAPDB_PASSWORD env vars (default)
  - FileProvider: reads from file (K8s mounted secrets)

- Add NewPasswordProvider factory (pkg/hsm/factory.go) driven by MPC_HSM_PROVIDER
  env var (values: aws, gcp, azure, env, file)

- Wire HSM provider into mpcd consensus startup, replacing direct env var read
  with graceful fallback chain: HSM provider -> viper config -> dev default

- Add --hsm-provider and --hsm-key-id CLI flags with MPC_HSM_PROVIDER/MPC_HSM_KEY_ID
  env var sources

- 25 tests passing (9 smart + 16 HSM)
2025-12-27 15:50:34 -08:00

248 lines
7.3 KiB
Go

package smart
import (
"encoding/hex"
"strings"
"testing"
)
// TestSafeEncodeDeploy verifies EncodeDeploy produces valid 4-byte selector + ABI data.
func TestSafeEncodeDeploy(t *testing.T) {
cfg := SafeConfig{
Owners: []string{"0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59"},
Threshold: 1,
Salt: "0x1",
}
calldata, err := EncodeDeploy(cfg)
if err != nil {
t.Fatalf("EncodeDeploy: %v", err)
}
if len(calldata) < 4 {
t.Fatalf("calldata too short: %d bytes", len(calldata))
}
// Selector for createProxyWithNonce(address,bytes,uint256)
wantSel := abiSelector("createProxyWithNonce(address,bytes,uint256)")
gotSel := calldata[:4]
if hex.EncodeToString(gotSel) != hex.EncodeToString(wantSel) {
t.Errorf("wrong selector: got %x want %x", gotSel, wantSel)
}
}
// TestSafeHashSafeTransaction verifies EIP-712 hash is 32 bytes and deterministic.
func TestSafeHashSafeTransaction(t *testing.T) {
tx := SafeTransaction{
To: "0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59",
Value: "1000000000000000000",
Nonce: 0,
}
hash1, err := HashSafeTransaction("0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", 1, tx)
if err != nil {
t.Fatalf("HashSafeTransaction: %v", err)
}
if len(hash1) != 32 {
t.Errorf("expected 32-byte hash, got %d bytes", len(hash1))
}
// Deterministic
hash2, _ := HashSafeTransaction("0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", 1, tx)
if hex.EncodeToString(hash1) != hex.EncodeToString(hash2) {
t.Error("hash not deterministic")
}
// Different chain ID → different hash
hash3, _ := HashSafeTransaction("0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", 137, tx)
if hex.EncodeToString(hash1) == hex.EncodeToString(hash3) {
t.Error("expected different hash for different chainID")
}
}
// TestSafePredictAddress verifies PredictAddress returns a valid hex address.
func TestSafePredictAddress(t *testing.T) {
cfg := SafeConfig{
Owners: []string{"0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59"},
Threshold: 1,
Salt: "0x0",
}
addr := PredictAddress(cfg)
if !strings.HasPrefix(addr, "0x") {
t.Errorf("expected 0x-prefixed address, got %q", addr)
}
if len(addr) != 42 {
t.Errorf("expected 42-char address, got %d chars: %s", len(addr), addr)
}
}
// TestEncodeExecTransaction verifies execTransaction calldata has correct selector.
func TestEncodeExecTransaction(t *testing.T) {
tx := SafeTransaction{
To: "0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59",
Value: "0",
}
sigs := PackSignature(make([]byte, 32), make([]byte, 32), 27)
calldata, err := EncodeExecTransaction(tx, sigs)
if err != nil {
t.Fatalf("EncodeExecTransaction: %v", err)
}
wantSel := abiSelector("execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)")
if hex.EncodeToString(calldata[:4]) != hex.EncodeToString(wantSel) {
t.Errorf("wrong selector: got %x want %x", calldata[:4], wantSel)
}
}
// TestBuildUserOp verifies BuildUserOp returns a valid UserOperation.
func TestBuildUserOp(t *testing.T) {
op := BuildUserOp(
"0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59",
"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552",
"1000000000000000000",
"",
0,
)
if op.Sender == "" {
t.Error("expected sender")
}
if !strings.HasPrefix(op.CallData, "0x") {
t.Error("expected 0x-prefixed calldata")
}
}
// TestHashUserOp verifies ERC-4337 userOpHash is 32 bytes and deterministic.
func TestHashUserOp(t *testing.T) {
op := BuildUserOp(
"0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59",
"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552",
"0",
"",
42,
)
hash1, err := HashUserOp(op, EntrypointV06, 1)
if err != nil {
t.Fatalf("HashUserOp: %v", err)
}
if len(hash1) != 32 {
t.Errorf("expected 32-byte hash, got %d", len(hash1))
}
hash2, _ := HashUserOp(op, EntrypointV06, 1)
if hex.EncodeToString(hash1) != hex.EncodeToString(hash2) {
t.Error("hash not deterministic")
}
}
// TestPredictAccountAddress verifies CREATE2 address prediction produces valid addresses.
func TestPredictAccountAddress(t *testing.T) {
cfg := AccountConfig{
FactoryAddress: "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
OwnerAddress: "0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59",
Salt: "0x0",
}
addr := PredictAccountAddress(cfg)
if !strings.HasPrefix(addr, "0x") {
t.Errorf("expected 0x-prefixed address, got %q", addr)
}
if len(addr) != 42 {
t.Errorf("expected 42-char address, got %d chars: %s", len(addr), addr)
}
// Same inputs must produce same address (deterministic)
addr2 := PredictAccountAddress(cfg)
if addr != addr2 {
t.Errorf("non-deterministic: %s != %s", addr, addr2)
}
// Different salt must produce different address
cfg2 := cfg
cfg2.Salt = "0x1"
addr3 := PredictAccountAddress(cfg2)
if addr == addr3 {
t.Errorf("different salt should produce different address: %s == %s", addr, addr3)
}
// Different owner must produce different address
cfg3 := cfg
cfg3.OwnerAddress = "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552"
addr4 := PredictAccountAddress(cfg3)
if addr == addr4 {
t.Errorf("different owner should produce different address: %s == %s", addr, addr4)
}
// Empty factory should return empty
cfg4 := cfg
cfg4.FactoryAddress = ""
if PredictAccountAddress(cfg4) != "" {
t.Error("expected empty string for empty factory")
}
// Empty owner should return empty
cfg5 := cfg
cfg5.OwnerAddress = ""
if PredictAccountAddress(cfg5) != "" {
t.Error("expected empty string for empty owner")
}
}
// TestPredictCreate2Address verifies the raw CREATE2 formula.
func TestPredictCreate2Address(t *testing.T) {
var deployer [20]byte
copy(deployer[:], []byte{0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01})
var salt [32]byte
salt[31] = 0x42
initCode := []byte{0x60, 0x80, 0x60, 0x40, 0x52} // minimal EVM bytecode
addr := PredictCreate2Address(deployer, salt, initCode)
// Must be non-zero
allZero := true
for _, b := range addr {
if b != 0 {
allZero = false
break
}
}
if allZero {
t.Error("expected non-zero address")
}
// Deterministic
addr2 := PredictCreate2Address(deployer, salt, initCode)
if addr != addr2 {
t.Errorf("non-deterministic: %x != %x", addr, addr2)
}
// Different salt -> different address
salt2 := salt
salt2[31] = 0x43
addr3 := PredictCreate2Address(deployer, salt2, initCode)
if addr == addr3 {
t.Errorf("different salt should produce different address")
}
}
// TestEncodeInitCode verifies the ERC-4337 initCode format.
func TestEncodeInitCode(t *testing.T) {
cfg := AccountConfig{
FactoryAddress: "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
OwnerAddress: "0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59",
Salt: "0x0",
}
initCode, err := EncodeInitCode(cfg)
if err != nil {
t.Fatalf("EncodeInitCode: %v", err)
}
// First 20 bytes = factory address
if len(initCode) < 20 {
t.Fatalf("initCode too short: %d bytes", len(initCode))
}
gotFactory := "0x" + hex.EncodeToString(initCode[:20])
wantFactory := strings.ToLower(cfg.FactoryAddress)
if strings.ToLower(gotFactory) != wantFactory {
t.Errorf("factory mismatch: got %s want %s", gotFactory, wantFactory)
}
// Bytes 20+ = createAccount selector + params
remaining := initCode[20:]
wantSel := abiSelector("createAccount(address,uint256)")
if len(remaining) < 4 || hex.EncodeToString(remaining[:4]) != hex.EncodeToString(wantSel) {
t.Errorf("wrong createAccount selector: got %x want %x", remaining[:4], wantSel)
}
}