欢迎光临
我们一直在努力

Starknet 签名指南

starknet 签名指南

抽象的

本文概述了 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: %sn", err)
        return
    }
    if !curve.curve.isoncurve(pubx, puby) {
        fmt.printf("point is not on curven")
        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.<a style="color:#f60; text-decoration:underline;" href="https://www.codesou.cn/" target="_blank">typedef</a>{
        "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 值并不总是提供。

验证签名

为了验证签名,从密码学的角度来看,需要公钥。然而,由于 starknet 中的帐户抽象,并不总是可以访问公钥。目前,无法通过浏览器钱包检索公钥。因此,验证签名的方法分为两种:使用用户的公钥(如果有)或使用用户的地址(即账户智能合约地址)。

使用用户的公钥

如果用户的公钥可用,则可以使用公钥验证签名。这是验证签名的代码。

打字稿:

// following the previous code
const isvalid = ec.starkcurve.verify(signature, messagehash, fullpublickey)

去:

// following the previous code
isvalid := curve.curve.verify(hash, r, s, starknetpublickey, puby)

使用用户地址

注意:仅当用户的账户智能合约已在 starknet 网络上部署(激活)时,此方法才有效。当用户创建帐户并需要一些汽油费时,这种部署通常是通过浏览器钱包完成的。使用浏览器钱包签名时,在javascript代码中指定skipdeploy参数。前面提供的示例代码不适用于与浏览器钱包不同的签名,因为使用了示例私钥来签署消息。

重要提示:在试验代码时避免使用您自己的私钥。始终使用浏览器钱包签署交易。

如果用户的公钥不可用,可以使用用户的账户智能合约来验证签名。根据标准src-6,用户账户智能合约有一个函数 fn is_valid_signature(hash: feel252, signature: array) -> feel252;它采用消息和签名的哈希值(以 2 个 feel252 值的数组形式:r 和 s),如果签名有效,则返回字符串 valid,否则返回失败。以下是在 typescript 和 go 中使用用户帐户地址验证签名的代码。

typescript(为了便于阅读而简化):

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 <a style="color:#f60; text-decoration:underline;" href="https://www.codesou.cn/" target="_blank">access</a> 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(为了便于阅读而简化):

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")

用法

签名可用于各种应用程序,其中 web3 dapp 中的用户身份验证是主要用例。为此,请使用上面提供的结构使用用户的帐户地址进行签名验证。这是完整的工作流程:

  1. 用户使用浏览器钱包签署消息。
  2. 将用户地址、消息和签名(r,s)发送到后端。
  3. 后端使用用户账户智能合约验证签名。

确保前后端消息结构相同,以确保签名验证正确。

结论

希望本文能让您对 starknet 上的签名有一个全面的了解,并帮助您在应用程序中实现它。如果您有任何问题或反馈,请随时发表评论或在 twitter 或 github 上与我联系。感谢您的阅读!

来源:

  • 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
赞(0) 打赏
未经允许不得转载:码农资源网 » Starknet 签名指南
分享到

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏

登录

找回密码

注册