首页  >  文章  >  后端开发  >  Starknet 签名指南

Starknet 签名指南

WBOY
WBOY原创
2024-07-21 11:13:49759浏览

A guide on Starknet signatures

抽象的

本文概述了在 Starknet 上签名和验证签名的过程。首先介绍账户抽象,以及与以太坊等传统区块链相比,它如何修改签名验证。然后,它提供了 TypeScript 和 Go 中的全面代码示例,用于使用 Starknet 上提供的两种方法对消息进行签名和验证签名:使用用户的公钥和使用用户的帐户地址。

现场签名游乐场可在 https://signatures.felts.xyz

本文中给出的所有代码示例都可以在关联的 GitHub 存储库中找到。我要感谢蒂亚戈对代码片段的帮助。

账户抽象

在以太坊中,个人用户帐户,称为外部拥有帐户(EOA),由一对私钥和公钥控制。交易需要私钥签名才能修改账户状态。虽然安全,但该系统也有明显的缺点,例如如果私钥丢失或被盗,资产将遭受不可逆转的损失,钱包功能有限,以及缺乏用户友好的密钥或帐户恢复选项。

Starknet 通过账户抽象 (AA) 解决了这些限制,它通过智能合约而不是私钥来管理账户。这种方法允许智能合约验证其交易,从而实现智能合约涵盖的汽油费、单个账户的多个签名者以及各种加密签名等功能。 AA 使开发人员能够设计自定义安全模型,例如用于日常和高价值交易的不同密钥以及用于增强安全性的生物识别身份验证,从而增强安全性和用户体验。它还通过社交恢复和基于硬件的交易签名等方法简化了密钥恢复和管理。此外,AA 支持密钥轮换、Web3 应用程序的会话密钥以及多种签名和验证方案,从而允许定制安全措施。通过解决以太坊 EOA 模型的固有局限性,Starknet 的 AA 提供了更灵活、安全、用户友好的账户管理方法,显着改善了区块链交互。

签名

了解了帐户抽象后,我们现在可以探索它如何改变签名验证。首先,必须了解签名的组成。 STARK曲线是一条椭圆曲线,其签名是ECDSA签名,由两个值组成:r和s。签名是通过使用私钥对消息进行签名而生成的,并且可以使用公钥进行验证。有关 ECDSA 签名的更多信息,请参阅维基百科页面。

签署消息

在 Starknet 中,要签名的消息通常遵循 EIP-712 格式。该消息格式包括四个强制字段:types、primaryType、domain 和 message。 types 字段将类型名称映射到其相应的类型定义。 PrimaryType 字段指定消息的主要类型。域字段包含指定链上下文的键值对。消息字段包括描述消息的键值对。我们通常将消息表示为 JSON 对象:

{
    types: {
        StarkNetDomain: [
            { name: "name", type: "felt" },
            { name: "chainId", type: "felt" },
            { name: "version", type: "felt" },
        ],
        Message: [{ name: "message", type: "felt" }],
    },
    primaryType: "Message",
    domain: {
        name: "MyDapp",
        chainId: "SN_MAIN",
        version: "0.0.1",
    },
    message: {
        message: "hello world!",
    },
}

要签署消息,您需要私钥。如需深入了解签名流程,请参考ECDSA签名算法。以下是签署消息的代码。

打字稿:

import { ec, encode, TypedData, Signer, typedData, WeierstrassSignatureType } from 'starknet';

//--------------------------------------------------------------------------
// Account
//--------------------------------------------------------------------------
const privateKey = '0x1234567890987654321';

const starknetPublicKey = ec.starkCurve.getStarkKey(privateKey);

const fullPublicKey = encode.addHexPrefix(
    encode.buf2hex(ec.starkCurve.getPublicKey(privateKey, false))
);

const pubX = starknetPublicKey
const pubY = encode.addHexPrefix(fullPublicKey.slice(68))

//--------------------------------------------------------------------------
// Message
//--------------------------------------------------------------------------

const messageStructure: TypedData = {
    types: {
        StarkNetDomain: [
            { name: "name", type: "felt" },
            { name: "chainId", type: "felt" },
            { name: "version", type: "felt" },
        ],
        Message: [{ name: "message", type: "felt" }],
    },
    primaryType: "Message",
    domain: {
        name: "MyDapp",
        chainId: "SN_MAIN",
        version: "0.0.1",
    },
    message: {
        message: "hello world!",
    },
};

const messageHash = typedData.getMessageHash(messageStructure, BigInt(starknetPublicKey))

//--------------------------------------------------------------------------
// Signature
//--------------------------------------------------------------------------

const signer = new Signer(privateKey)

let signature: WeierstrassSignatureType;
try {
    signature = (await signer.signMessage(messageStructure, starknetPublicKey)) as WeierstrassSignatureType
} catch (error) {
    console.error("Error signing the message:", error);
}

// signature has properties r and s

去:

package main

import (
    "fmt"
    "math/big"
    "strconv"

    "github.com/NethermindEth/starknet.go/curve"
    "github.com/NethermindEth/starknet.go/typed"
    "github.com/NethermindEth/starknet.go/utils"
)

// NOTE: at the time of writing, starknet.go forces us to create a custom
// message type as well as a method to format the message encoding since
// there is no built-in generic way to encode messages.
type MessageType struct {
    Message string
}

// FmtDefinitionEncoding is a method that formats the encoding of the message
func (m MessageType) FmtDefinitionEncoding(field string) (fmtEnc []*big.Int) {
    if field == "message" {
        if v, err := strconv.Atoi(m.Message); err == nil {
            fmtEnc = append(fmtEnc, big.NewInt(int64(v)))
        } else {
            fmtEnc = append(fmtEnc, utils.UTF8StrToBig(m.Message))
        }
    }
    return fmtEnc
}

func main() {
    //--------------------------------------------------------------------------
    // Account
    //--------------------------------------------------------------------------
    privateKey, _ := new(big.Int).SetString("1234567890987654321", 16)

    pubX, pubY, err := curve.Curve.PrivateToPoint(privateKey)
    if err != nil {
        fmt.Printf("Error: %s\n", err)
        return
    }
    if !curve.Curve.IsOnCurve(pubX, pubY) {
        fmt.Printf("Point is not on curve\n")
        return
    }

    starknetPublicKey := pubX

    // IMPORTANT: this is not a standard way to retrieve the full public key, it
    // is just for demonstration purposes as starknet.go does not provide a way
    // to retrieve the full public key at the time of writing.
    // Rule of thumb: never write your own cryptography code!
    fullPublicKey := new(big.Int).SetBytes(append(append(
        []byte{0x04},                       // 0x04 is the prefix for uncompressed public keys
        pubX.Bytes()...), pubY.Bytes()...), // concatenate x and y coordinates
    )

    //--------------------------------------------------------------------------
    // Message
    //--------------------------------------------------------------------------

    types := map[string]typed.TypeDef{
        "StarkNetDomain": {
            Definitions: []typed.Definition{
                {Name: "name", Type: "felt"},
                {Name: "chainId", Type: "felt"},
                {Name: "version", Type: "felt"},
            },
        },
        "Message": {
            Definitions: []typed.Definition{
                {Name: "message", Type: "felt"},
            },
        },
    }

    primaryType := "Message"

    domain := typed.Domain{
        Name:    "MyDapp",
        ChainId: "SN_MAIN",
        Version: "0.0.1",
    }

    message := MessageType{
        Message: "hello world!",
    }

    td, err := typed.NewTypedData(types, primaryType, domain)
    if err != nil {
        fmt.Println("Error creating TypedData:", err)
        return
    }

    hash, err := td.GetMessageHash(starknetPublicKey, message, curve.Curve)
    if err != nil {
        fmt.Println("Error getting message hash:", err)
        return
    }

    //--------------------------------------------------------------------------
    // Signature
    //--------------------------------------------------------------------------

    r, s, err := curve.Curve.Sign(hash, privateKey)
    if err != nil {
        fmt.Println("Error signing message:", err)
        return
    }
}

如果您正在开发 dApp,您将无权访问用户的私钥。相反,您可以使用 starknet.js 库对消息进行签名。该代码将与浏览器钱包(通常是 ArgentX 或 Braavos)交互以对消息进行签名。您可以在 https://signatures.felts.xyz 找到现场演示。以下是使用浏览器钱包在 TypeScript 中签署消息的简化代码(完整代码可在 GitHub 存储库中找到):

import { connect } from "get-starknet";

const starknet = await connect(); // Connect to the browser wallet

const messageStructure: TypedData = {
    types: {
        StarkNetDomain: [
            { name: "name", type: "felt" },
            { name: "chainId", type: "felt" },
            { name: "version", type: "felt" },
        ],
        Message: [{ name: "message", type: "felt" }],
    },
    primaryType: "Message",
    domain: {
        name: "MyDapp",
        chainId: "SN_MAIN",
        version: "0.0.1",
    },
    message: {
        message: "hello world!",
    },
};

// skipDeploy allows not-deployed accounts to sign messages
const signature = await starknet.account.signMessage(messageStructure, { skipDeploy: true });

一旦消息被签名,就会以r、s、v的形式获得签名。v值是恢复id,可用于从签名中恢复公钥(更多信息请参见维基百科) )。然而,除非事先知道签名者的公钥,否则不能完全信任此恢复过程来验证签名。 r和s值是用于验证签名的签名值。

重要提示:根据浏览器钱包的不同,签名可能只返回 r 和 s 值。 v 值并不总是提供。

Verifying a Signature

To verify a signature, the public key is required from a cryptographic perspective. However, due to Account Abstraction in Starknet, access to the public key is not always available. Currently, the public key cannot be retrieved through the browser wallet. Therefore, two methods are distinguished for verifying a signature: using the user's public key (if available) or using the user's address (i.e., account smart contract address).

Using the User's Public Key

If the user's public key is available, the signature can be verified using the public key. Here is the code to verify a signature.

TypeScript:

// following the previous code
const isValid = ec.starkCurve.verify(signature, messageHash, fullPublicKey)

Go:

// following the previous code
isValid := curve.Curve.Verify(hash, r, s, starknetPublicKey, pubY)

Using the User's Address

NOTE: This method works only if the user's account smart contract has been deployed (activated) on the Starknet network. This deployment is typically done through the browser wallet when the user creates an account and requires some gas fees. The skipDeploy parameter is specified in the JavaScript code when signing with the browser wallet. The example code provided earlier will not work with signatures different from the browser wallet since a sample private key was used to sign the message.

IMPORTANT: Avoid using your own private key when experimenting with the code. Always sign transactions with the browser wallet.

If the user's public key is not available, the signature can be verified using the user's account smart contract. By the standard SRC-6, the user account smart contract has a function fn is_valid_signature(hash: felt252, signature: Array) -> felt252; that takes the hash of the message and the signature (in the form of an array of 2 felt252 values: r and s) and returns the string VALID if the signature is valid, or fails otherwise. Here is the code to verify a signature using the user's account address in TypeScript and Go.

TypeScript (simplified for readability):

import { Account, RpcProvider } from "starknet";

const provider = new RpcProvider({ nodeUrl: "https://your-rpc-provider-url" });

// '0x123' is a placeholder for the user's private key since we don't have access to it
const account = new Account(provider, address, '0x123')

try {
    // messageStructure and signature are obtained from the previous code when signing the message with the browser wallet
    const isValid = account.verifyMessage(messageStructure, signature)
    console.log("Signature is valid:", isValid)
} catch (error) {
    console.error("Error verifying the signature:", error);
}

Go (simplified for readability):

import (
    "context"
    "encoding/hex"
    "fmt"
    "math/big"

    "github.com/NethermindEth/juno/core/felt"
    "github.com/NethermindEth/starknet.go/curve"
    "github.com/NethermindEth/starknet.go/rpc"
    "github.com/NethermindEth/starknet.go/utils"
)

...

provider, err := rpc.NewProvider("https://your-rpc-provider-url")
if err != nil {
    // handle error
}

// we import the account address, r, and s values from the frontend (typescript)
accountAddress, _ := new(big.Int).SetString("0xabc123", 16)
r, _ := new(big.Int).SetString("0xabc123", 16)
s, _ := new(big.Int).SetString("0xabc123", 16)

// we need to get the message hash, but, this time, we use the account address instead of the public key. `message` is the same as the in the previous Go code
hash, err := td.GetMessageHash(accountAddress, message, curve.Curve)
if err != nil {
    // handle error
}

callData := []*felt.Felt{
    utils.BigIntToFelt(hash),
    (&felt.Felt{}).SetUint64(2), // size of the array [r, s]
    utils.BigIntToFelt(r),
    utils.BigIntToFelt(s),
}

tx := rpc.FunctionCall{
    ContractAddress: utils.BigIntToFelt(accountAddress),
    EntryPointSelector: utils.GetSelectorFromNameFelt(
        "is_valid_signature",
    ),
    Calldata: callData,
}

result, err := provider.Call(context.Background(), tx, rpc.BlockID{Tag: "latest"})
if err != nil {
    // handle error
}

isValid, err := hex.DecodeString(result[0].Text(16))
if err != nil {
    // handle error
}

fmt.Println("Signature is valid:", string(isValid) == "VALID")

Usage

Signatures can be used in various applications, with user authentication in web3 dApps being a primary use case. To achieve this, use the structure provided above for signature verification using the user's account address. Here is the complete workflow:

  1. The user signs a message with the browser wallet.
  2. Send the user address, message, and signature (r, s) to the backend.
  3. The backend verifies the signature using the user's account smart contract.

Make sure that the message structure is the same on the frontend and backend to ensure the signature is verified correctly.

Conclusion

I hope that this article provided you with a comprehensive understanding of the signatures on Starknet and helped you implement it in your applications. If you have any questions or feedback, feel free to comment or reach out to me on Twitter or GitHub. Thank you for reading!

Sources:

  • https://book.starknet.io/ch04-00-account-abstraction.html
  • https://www.starknetjs.com/docs/guides/signature/
  • https://docs.starknet.io/architecture-and-concepts/accounts/introduction/
  • https://docs.openzeppelin.com/contracts-cairo/0.4.0/accounts#isvalidsignature
  • https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
  • https://eips.ethereum.org/EIPS/eip-712

以上是Starknet 签名指南的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn