首頁 > 後端開發 > Golang > 主體

Starknet 簽名指南

WBOY
發布: 2024-07-21 11:13:49
原創
833 人瀏覽過

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中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板