>  기사  >  백엔드 개발  >  Starknet 서명에 대한 가이드

Starknet 서명에 대한 가이드

WBOY
WBOY원래의
2024-07-21 11:13:49760검색

A guide on Starknet signatures

추상적인

이 기사에서는 Starknet에서 서명하고 서명을 확인하는 과정을 간략하게 설명합니다. 계정 추상화를 소개하고 이더리움과 같은 기존 블록체인과 비교하여 서명 확인을 수정하는 방법으로 시작됩니다. 그런 다음 Starknet에서 사용할 수 있는 두 가지 방법(사용자의 공개 키 사용 및 사용자 계정 주소 사용)을 사용하여 메시지에 서명하고 서명을 확인하기 위한 TypeScript 및 Go의 포괄적인 코드 예제를 제공합니다.

실시간 시그니처 플레이그라운드는 https://signatures.felts.xyz에서 보실 수 있습니다

이 기사에 제공된 모든 코드 예제는 관련 GitHub 저장소에서 사용할 수 있습니다. 코드 조각에 도움을 준 Thiago에게 감사 인사를 전하고 싶습니다.

계정 추상화

이더리움에서는 외부 소유 계정(EOA)으로 알려진 개별 사용자 계정이 개인 키와 공개 키 쌍으로 제어됩니다. 거래에서 계정 상태를 수정하려면 개인 키의 서명이 필요합니다. 이 시스템은 안전하지만 개인 키를 분실하거나 도난당한 경우 되돌릴 수 없는 자산 손실, 지갑 기능 제한, 사용자 친화적인 키 또는 계정 복구 옵션 부족 등 심각한 단점이 있습니다.

스타크넷은 개인 키 대신 스마트 계약을 통해 계정을 관리하는 계정 추상화(AA)를 통해 이러한 제한 사항을 해결합니다. 이 접근 방식을 통해 스마트 계약은 거래를 검증하고 스마트 계약에 포함되는 가스 요금, 단일 계정에 대한 여러 서명자 및 다양한 암호화 서명과 같은 기능을 활성화할 수 있습니다. AA는 개발자가 루틴 및 고가치 트랜잭션을 위한 다양한 키, 강화된 보안을 위한 생체 인식 인증과 같은 맞춤형 보안 모델을 설계할 수 있도록 함으로써 보안 및 사용자 경험을 향상시킵니다. 또한 소셜 복구 및 하드웨어 기반 트랜잭션 서명과 같은 방법을 통해 키 복구 및 관리를 단순화합니다. 또한 AA는 키 순환, web3 애플리케이션용 세션 키, 다양한 서명 및 검증 체계를 지원하여 맞춤형 보안 조치를 허용합니다. 이더리움 EOA 모델의 본질적인 한계를 해결함으로써 Starknet의 AA는 계정 관리에 대한 보다 유연하고 안전하며 사용자 친화적인 접근 방식을 제공하여 블록체인 상호 작용을 크게 향상시킵니다.

서명

계정 추상화에 대한 이해를 바탕으로 이제 서명 확인이 어떻게 변경되는지 알아볼 수 있습니다. 첫째, 시그니처의 구성을 이해하는 것이 필수적이다. STARK 곡선은 타원 곡선이고 그 서명은 r과 s라는 두 값으로 구성된 ECDSA 서명입니다. 서명은 개인 키로 메시지에 서명하여 생성되며 공개 키를 사용하여 확인할 수 있습니다. ECDSA 서명에 대한 자세한 내용은 Wikipedia 페이지를 참조하세요.

메시지에 서명하기

Starknet에서 서명할 메시지는 일반적으로 EIP-712 형식을 따릅니다. 이 메시지 형식에는 유형, 기본 유형, 도메인 및 메시지의 네 가지 필수 필드가 포함됩니다. 유형 필드는 유형 이름을 해당 유형 정의에 매핑합니다. 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입니다(자세한 내용은 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.