钱包签名机 - 交易所地址生成业务及签名机项目搭建

钱包地址生成业务,通常指的是根据特定区块链协议规范,从一对密钥(公钥、私钥)生成唯一的钱包地址的过程。这是区块链系统中非常核心的基础设施服务之一,广泛用于交易所、托管服务、Web3项目等。

钱包地址生成业务

钱包地址生成业务,通常指的是根据特定区块链协议规范,从一对密钥(公钥、私钥)生成唯一的钱包地址的过程。这是区块链系统中非常核心的基础设施服务之一,广泛用于交易所、托管服务、Web3 项目等。

结构图

image.png 对于以上流程图,首先是交易所的钱包层地址生成定时任务会一直监听数据库中地址是否足够,然后去请求签名机去调用批量地址生成业务,获得公钥后,转化成地址存储在钱包层的数据库中。

对于一个新增用户而言,他所请求的业务首先会进入到交易所的业务层,业务层将这个新增用户的请求转发给到钱包层这边。钱包层收到了这个请求则回去数据库中匹配一个地址分配给到这个用户。

对于交易所而言,地址生成业务是有其用户量决定的。假设,某交易所日新增用户 100 人左右,那么一周的用户量就是 700 人。一周批量生成一次 1000 个地址是绰绰有余的(当然还会经过数据库剩余地址数量检测)

签名机实战

下面我们来使用 go 语言搭建一个签名机应用,提供 ECDSAEdDSA 地址批量生成能力,并支持消息签名。

完整代码仓库: signature-machine

项目结构图

image.png 在这个项目中我们使用到了 urfave/cli/v2 库来搭建控制台应用,使用 leveldb 作为嵌入式数据库,使用 Grpc 提供与钱包层应用的交互能力,使用 ecdsa 的库和 eddsa 的库来保证地址生成和签名的能力。

在这个应用中,我们编写三个重要的接口:

  1. 批量公钥导出
  2. 签名
  3. 验证签名

第一步:搭建环境

  • go mod init 一个空环境
  • git init 托管到 github,新建 .gitignore 文件用于排除 github 提交文件
  • 新建 .env 文件用于环境变量读取
export SIGNATURE_RPC_PORT=8983
export SIGNATURE_RPC_HOST="127.0.0.1"
export SIGNATURE_LEVEL_DB_PATH="./data"
  • 新建 bin/compile.sh 用于管理 protobuf 的生成命令

第二步:搭建控制台应用框架

  • 建立文件夹 cmd/signature,并新建 main.go 文件,这是程序的主入口
func main() {
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true)))
    app := NewCli(GitCommit, GitData)
    ctx := opio.WithInterruptBlocker(context.Background())
    if err := app.RunContext(ctx, os.Args); err != nil {
        log.Error("Application failed")
        os.Exit(1)
    }
}
  • 相同文件夹下建立 cli.go,使用 urfave/cle/v2 作为命令行应用的框架
func NewCli(GitCommit string, GitData string) *cli.App {
    flags := flags.Flags
    return &cli.App{
        Version:              params.VersionWithCommit(GitCommit, GitData),
        Description:          "Signature machine eth tool",
        EnableBashCompletion: true,
        Commands: []*cli.Command{

            {
                Name:        "rpc",
                Flags:       flags,
                Description: "Run RPC",
                Action:      cliapp.LifecycleCmd(runRpc),
            },
        },
    }
}
  • 新建 Makefile 文件用于管理 shell 命令(build/test/lint/proto/clean)
  • 测试 version 命令是否正常运行(./signature version) image.png

    第三步:搭建 RPC 框架

  • 编写 proto 文件,定义好接口、输入输出消息
service WalletService {
  rpc getSupportSignWay(SupportSignWayRequest) returns (SupportSignWayResponse) {}
  rpc exportPublicKeyList(ExportPublicKeyRequest) returns (ExportPublicKeyResponse) {}
  rpc signTxMessage(SignTxMessageRequest) returns (SignTxMessageResponse) {}
}
  • proto 生成 go 代码,放入 protobuf 包内
make proto
  • 读取配置,包括 GrpchostportLevelDB 的路径
func NewConfig(ctx *cli.Context) Config {
    return Config{
        LevelDbPath: ctx.String(flags.LevelDbPathFlag.Name),
        RpcServer: ServerConfig{
            Host: ctx.String(flags.RpcHostFlag.Name),
            Port: ctx.Int(flags.RpcPortFlag.Name),
        },
    }
}
  • 打开 LevelDB 连接
func NewKeyStore(path string) (*Keys, error) {
    db, err := NewLevelStore(path)
    if err != nil {
        log.Error("Could not open leveldb", "path", path, "err", err)
        return nil, err
    }
    return &Keys{
        db: db,
    }, nil
}
  • 启动 GRPC 服务,注册 proto 定义好的路由进去
        gs := grpc.NewServer(
            opt,
            grpc.ChainUnaryInterceptor(nil),
        )
        reflection.Register(gs)

        wallet.RegisterWalletServiceServer(gs, r)

第四步:实现接口

  • 新建 ssm 包,负责封装 ecdsaeddsa 的秘钥生成、消息签名
  • 创建 ECDSA 秘钥对

    func CreateECDSAKeyPair() (string, string, string, error) {
    privateKey, err := crypto.GenerateKey()
    if err != nil {
        log.Error("generate key fail", "err", err)
        return EmptyHexString, EmptyHexString, EmptyHexString, err
    }
    priKeyStr := hex.EncodeToString(crypto.FromECDSA(privateKey))
    pubKeyStr := hex.EncodeToString(crypto.FromECDSAPub(&privateKey.PublicKey))
    compressPubkeyStr := hex.EncodeToString(crypto.CompressPubkey(&privateKey.PublicKey))
    
    return priKeyStr, pubKeyStr, compressPubkeyStr, nil
    }

    handle.go 中实现接口

  • 获取加密支持方式(ecdsaeddsa): 返回是否支持该方式的地址生成和签名
// 获取加密支持类型
func (r *RpcServer) GetSupportSignWay(ctx context.Context, request *wallet.SupportSignWayRequest) (*wallet.SupportSignWayResponse, error) {
    var signWay []*wallet.SignWay
    signWay = append(signWay, &wallet.SignWay{Schema: "ecdsa"})
    signWay = append(signWay, &wallet.SignWay{Schema: "eddsa"})
    return &wallet.SupportSignWayResponse{
        Code:    wallet.ReturnCode_SUCCESS,
        Msg:     "get sign way success",
        SignWay: signWay,
    }, nil
}
  • 批量导出公钥: 根据请求的方式(ecdsaeddsa)的不同,分别调用 ecdsaeddsa 的秘钥对生成方法, 生成出对应秘钥对。 将私钥和公钥保存到 leveldb 中,将公钥返回给调用方
// 批量导出公钥
func (r *RpcServer) ExportPublicKeyList(ctx context.Context, request *wallet.ExportPublicKeyRequest) (*wallet.ExportPublicKeyResponse, error) {
    resp := &wallet.ExportPublicKeyResponse{
        Code: wallet.ReturnCode_SUCCESS,
    }
    cryptoType, err := protobuf.ParseTransactionType(request.Type)
    if err != nil {
        resp.Msg = "input type error"
        return resp, nil
    }
    if request.Number > 10000 {
        resp.Msg = "input number > 10000"
        return resp, nil
    }

    var keyList []leveldb.Key
    var retKeyList []*wallet.PublicKey

    for counter := 0; counter < int(request.Number); counter++ {
        var privateKeyStr, publicKeyStr, compressPublicKeyStr string
        var err error

        switch cryptoType {
        case protobuf.ECDSA:
            privateKeyStr, publicKeyStr, compressPublicKeyStr, err = ssm.CreateECDSAKeyPair()
        case protobuf.EDDSA:
            privateKeyStr, publicKeyStr, err = ssm.CreateEdDSAKeyPair()
            compressPublicKeyStr = publicKeyStr
        default:
            return nil, errors.New("unsupported key type")
        }
        if err != nil {
            log.Error("create key pair error", "err", err)
            return nil, err
        }

        keyItem := leveldb.Key{
            PrivateKey: privateKeyStr,
            PublicKey:  publicKeyStr,
        }
        putItem := &wallet.PublicKey{
            CompressPubkey: compressPublicKeyStr,
            Pubkey:         publicKeyStr,
        }
        retKeyList = append(retKeyList, putItem)
        keyList = append(keyList, keyItem)
    }
    isOk := r.db.StoreKeys(keyList)
    if !isOk {
        log.Error("db store keys error", "err", isOk)
        return nil, errors.New("db store keys error")
    }
    resp.Code = wallet.ReturnCode_SUCCESS
    resp.Msg = "create key pair success"
    resp.PublicKey = retKeyList
    return resp, nil
}
  • 交易签名: 请求方提供加密方式、messageHash、公钥,请求签名。签名机内部调用 ecdsaeddsa 的 签名方法将 messageHash 签名,返回 signature
// 通过 messageHash 进行签名
func (r *RpcServer) SignTxMessage(ctx context.Context, request *wallet.SignTxMessageRequest) (*wallet.SignTxMessageResponse, error) {
    resp := &wallet.SignTxMessageResponse{
        Code: wallet.ReturnCode_ERROR,
    }
    cryptoType, err := protobuf.ParseTransactionType(request.Type)
    if err != nil {
        resp.Msg = "input type error"
        return resp, nil
    }
    privateKey, isOk := r.db.GetPrivateKey(request.PublicKey)
    if !isOk {
        return nil, errors.New("get private key error")
    }

    var signature string
    var err2 error
    switch cryptoType {
    case protobuf.ECDSA:
        signature, err2 = ssm.SignECDSAMessage(privateKey, request.MessageHash)
    case protobuf.EDDSA:
        signature, err2 = ssm.SignEdDSAMessage(privateKey, request.MessageHash)
    default:
        return nil, errors.New("unsupported key type")
    }
    if err2 != nil {
        return nil, err2
    }
    resp.Code = wallet.ReturnCode_SUCCESS
    resp.Msg = "sign tx message success"
    resp.Signature = signature
    return resp, nil
}

测试

构建应用

make

image.png

  • 启动
source .env
./signature rpc

image.png

  • 另一终端中启动 grpc UI
    grpcui -plaintext 127.0.0.1:8983

image.png

image.png

1.链支持

request

grpcurl -plaintext -d '{}' 127.0.0.1:8983 wallet.WalletService.getSupportSignWay

response

{
  "Code": "SUCCESS",
  "msg": "get sign way success",
  "sign_way": [
    {
      "schema": "ecdsa"
    },
    {
      "schema": "eddsa"
    }
  ]
}

2.批量地址生成

request

grpcurl -plaintext -d '{
  "type": "ecdsa",
  "number": "3"
}' 127.0.0.1:8983 wallet.WalletService.exportPublicKeyList

response

{
  "Code": "SUCCESS",
  "msg": "create key pair success",
  "public_key": [
    {
      "compress_pubkey": "0287916d6ff3cc4312c8e996bfbff4f8d2f7cf44da480f3a2e70be64bdebaa68f0",
      "pubkey": "0487916d6ff3cc4312c8e996bfbff4f8d2f7cf44da480f3a2e70be64bdebaa68f0e7ca2c7409fc0edb4d5f0496c3a9de1a0904deb48fe329f9a9130e5a70f35e76"
    },
    {
      "compress_pubkey": "031427c8d6062b79072a0b941a87f776f4b862e6ab9f9b7fa5a61f5b7b3722e9ab",
      "pubkey": "041427c8d6062b79072a0b941a87f776f4b862e6ab9f9b7fa5a61f5b7b3722e9ab1b3ec09bf22c123f2398bab7ecab516f7508c25bc0d506c52ee6924403eb701b"
    },
    {
      "compress_pubkey": "03ed79ebca46736aa456729d38d00f87d5c435987adcebc2d6404f4fefc13bb72a",
      "pubkey": "04ed79ebca46736aa456729d38d00f87d5c435987adcebc2d6404f4fefc13bb72a6811f36574c2fcc42c66903c9309a94dadc4d2e74d39b4c58e5979fca189c905"
    }
  ]
}

3.签名

request

grpcurl -plaintext -d '{
  "type": "ecdsa",
  "publicKey": "04ed79ebca46736aa456729d38d00f87d5c435987adcebc2d6404f4fefc13bb72a6811f36574c2fcc42c66903c9309a94dadc4d2e74d39b4c58e5979fca189c905",
  "messageHash": "0x9ca77bd43a45da2399da96159b554bebdd89839eec73a8ff0626abfb2fb4b538"
}' 127.0.0.1:8983 wallet.WalletService.signTxMessage

response

{
  "Code": "SUCCESS",
  "msg": "sign tx message success",
  "signature": "d703844145ef3277b9c8abf6e878590b69122684b2985bb75fd5454177a9ccfb19efcca6c063ce94ca71f3164d9d83d082fe7df73a6afbde579ff29fce4738e800"
}
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
shawn_shaw
shawn_shaw
web3潜水员、技术爱好者、web3钱包开发工程师。欢迎闲聊唠嗑、精进技术、交流工作机会。vx:cola_ocean