ホームページ  >  記事  >  バックエンド開発  >  Starknet 署名に関するガイド

Starknet 署名に関するガイド

WBOY
WBOYオリジナル
2024-07-21 11:13:49839ブラウズ

A guide on Starknet signatures

抽象的な

この記事では、Starknet での署名と署名の検証のプロセスの概要を説明します。まず、アカウントの抽象化を紹介し、イーサリアムなどの従来のブロックチェーンと比較して署名検証がどのように変更されるかについて説明します。次に、Starknet で利用可能な 2 つの方法 (ユーザーの公開キーを使用する方法とユーザーのアカウント アドレスを使用する方法) を使用してメッセージに署名し、署名を検証するための TypeScript と Go の包括的なコード例を示します。

ライブ署名プレイグラウンドは https://signatures.felts.xyz で利用できます

この記事に記載されているすべてのコード例は、関連する GitHub リポジトリで入手できます。コード スニペットに関して協力してくれた Thiago に感謝します。

アカウントの抽象化

イーサリアムでは、外部所有アカウント (EOA) として知られる個々のユーザー アカウントは、秘密鍵と公開鍵のペアによって制御されます。トランザクションでは、アカウントの状態を変更するために秘密キーからの署名が必要です。このシステムは安全ではありますが、秘密鍵が紛失または盗難された場合に資産が元に戻せないこと、ウォレットの機能が制限されていること、ユーザーフレンドリーな鍵やアカウントの回復オプションが欠如していることなど、重大な欠点があります。

Starknet は、秘密キーの代わりにスマート コントラクトを通じてアカウントを管理するアカウント抽象化 (AA) を通じてこれらの制限に対処します。このアプローチにより、スマート コントラクトがトランザクションを検証できるようになり、スマート コントラクトでカバーされるガス料金、単一アカウントの複数の署名者、さまざまな暗号署名などの機能が有効になります。 AA は、開発者が日常的な高額取引用の異なるキーやセキュリティを強化するための生体認証などのカスタム セキュリティ モデルを設計できるようにすることで、セキュリティとユーザー エクスペリエンスを強化します。また、ソーシャル リカバリやハードウェア ベースのトランザクション署名などの方法を使用して、キーのリカバリと管理を簡素化します。さらに、AA はキーのローテーション、Web3 アプリケーションのセッション キー、さまざまな署名と検証スキームをサポートしており、カスタマイズされたセキュリティ対策を可能にします。イーサリアムの EOA モデルに固有の制限に対処することで、Starknet の AA は、アカウント管理に対するより柔軟で安全かつユーザーフレンドリーなアプローチを提供し、ブロックチェーンの相互作用を大幅に改善します。

サイン

アカウントの抽象化を理解したので、それが署名の検証にどのように変化するかを調べることができます。まず、署名の構成を理解することが重要です。 STARK 曲線は楕円曲線であり、その署名は ECDSA 署名であり、r と s の 2 つの値で構成されます。署名は、秘密キーを使用してメッセージに署名することによって生成され、公開キーを使用して検証できます。 ECDSA 署名の詳細については、Wikipedia のページを参照してください。

メッセージに署名する

Starknet では、署名されるメッセージは通常、EIP-712 形式に従います。このメッセージ形式には、types、primaryType、domain、message の 4 つの必須フィールドが含まれます。 type フィールドは、型名を対応する型定義にマップします。 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 署名アルゴリズムを参照してください。以下はメッセージに署名するコードです。

TypeScript:

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 であり、署名から公開キーを回復するために使用できます (詳細については Wikipedia を参照してください) )。ただし、署名者の公開鍵が事前にわかっていない限り、この回復プロセスは署名の検証として完全に信頼することはできません。 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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。