Heim  >  Artikel  >  Backend-Entwicklung  >  Ein Leitfaden zu Starknet-Signaturen

Ein Leitfaden zu Starknet-Signaturen

WBOY
WBOYOriginal
2024-07-21 11:13:49823Durchsuche

A guide on Starknet signatures

Abstrakt

In diesem Artikel wird der Prozess des Signierens und Überprüfens einer Signatur auf Starknet beschrieben. Es beginnt mit der Einführung der Kontoabstraktion und der Art und Weise, wie sie die Signaturüberprüfung im Vergleich zu herkömmlichen Blockchains wie Ethereum verändert. Anschließend werden umfassende Codebeispiele in TypeScript und Go zum Signieren einer Nachricht und zum Überprüfen einer Signatur mit zwei auf Starknet verfügbaren Methoden bereitgestellt: Verwendung des öffentlichen Schlüssels des Benutzers und Verwendung der Kontoadresse des Benutzers.

Ein Live-Signatur-Spielplatz ist unter https://signatures.felts.xyz verfügbar

Alle in diesem Artikel aufgeführten Codebeispiele sind im zugehörigen GitHub-Repository verfügbar. Ich möchte Thiago für seine Hilfe bei den Codeschnipseln danken.

Kontoabstraktion

In Ethereum werden einzelne Benutzerkonten, sogenannte Externally Owned Accounts (EOAs), durch ein Paar privater und öffentlicher Schlüssel kontrolliert. Transaktionen erfordern eine Signatur des privaten Schlüssels, um den Kontostatus zu ändern. Dieses System ist zwar sicher, weist jedoch erhebliche Nachteile auf, wie z. B. einen irreversiblen Vermögensverlust bei Verlust oder Diebstahl des privaten Schlüssels, eingeschränkte Wallet-Funktionalität und das Fehlen benutzerfreundlicher Optionen zur Schlüssel- oder Kontowiederherstellung.

Starknet behebt diese Einschränkungen durch Account Abstraction (AA), das Konten über Smart Contracts statt über private Schlüssel verwaltet. Dieser Ansatz ermöglicht Smart Contracts die Validierung ihrer Transaktionen und ermöglicht Funktionen wie die von Smart Contracts abgedeckten Gasgebühren, mehrere Unterzeichner für ein einzelnes Konto und verschiedene kryptografische Signaturen. AA verbessert die Sicherheit und das Benutzererlebnis, indem es Entwicklern ermöglicht, benutzerdefinierte Sicherheitsmodelle zu entwerfen, z. B. unterschiedliche Schlüssel für Routine- und Transaktionen mit hohem Wert sowie biometrische Authentifizierung für mehr Sicherheit. Es vereinfacht auch die Wiederherstellung und Verwaltung von Schlüsseln mit Methoden wie Social Recovery und hardwarebasierter Transaktionssignierung. Darüber hinaus unterstützt AA Schlüsselrotation, Sitzungsschlüssel für Web3-Anwendungen sowie verschiedene Signatur- und Validierungsschemata und ermöglicht so maßgeschneiderte Sicherheitsmaßnahmen. Durch die Beseitigung der inhärenten Einschränkungen des EOA-Modells von Ethereum bietet Starknets AA einen flexibleren, sichereren und benutzerfreundlicheren Ansatz für die Kontoverwaltung, wodurch die Blockchain-Interaktionen erheblich verbessert werden.

Unterschrift

Mit einem Verständnis der Kontoabstraktion können wir nun untersuchen, wie sie die Signaturüberprüfung verändert. Zunächst ist es wichtig, den Aufbau einer Signatur zu verstehen. Die STARK-Kurve ist eine elliptische Kurve und ihre Signaturen sind ECDSA-Signaturen, die aus zwei Werten bestehen: r und s. Die Signatur wird durch das Signieren einer Nachricht mit dem privaten Schlüssel generiert und kann mithilfe des öffentlichen Schlüssels überprüft werden. Weitere Informationen zu ECDSA-Signaturen finden Sie auf der Wikipedia-Seite.

Eine Nachricht signieren

In Starknet folgen zu signierende Nachrichten normalerweise dem EIP-712-Format. Dieses Nachrichtenformat umfasst vier Pflichtfelder: Typen, Primärtyp, Domäne und Nachricht. Das Feld „Typen“ ordnet Typnamen den entsprechenden Typdefinitionen zu. Das Feld „primaryType“ gibt den primären Typ der Nachricht an. Das Domänenfeld enthält Schlüssel-Wert-Paare, die den Kettenkontext angeben. Das Nachrichtenfeld enthält Schlüssel-Wert-Paare, die die Nachricht beschreiben. Normalerweise stellen wir die Nachricht als JSON-Objekt dar:

{
    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!",
    },
}

Um eine Nachricht zu signieren, benötigen Sie den privaten Schlüssel. Weitere Informationen zum Signaturprozess finden Sie im ECDSA-Signaturalgorithmus. Unten finden Sie den Code zum Signieren einer Nachricht.

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

Los:

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
    }
}

Wenn Sie eine dApp entwickeln, haben Sie keinen Zugriff auf den privaten Schlüssel des Benutzers. Stattdessen können Sie die Bibliothek starknet.js verwenden, um die Nachricht zu signieren. Der Code interagiert mit der Browser-Wallet (typischerweise ArgentX oder Braavos), um die Nachricht zu signieren. Eine Live-Demo finden Sie unter https://signatures.felts.xyz. Hier ist der vereinfachte Code zum Signieren einer Nachricht in TypeScript mithilfe der Browser-Wallet (vollständiger Code im GitHub-Repository verfügbar):

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 });

Sobald die Nachricht signiert ist, wird die Signatur in Form von r, s und v erhalten. Der v-Wert ist die Wiederherstellungs-ID, die zum Wiederherstellen des öffentlichen Schlüssels aus der Signatur verwendet werden kann (weitere Informationen finden Sie auf Wikipedia). ). Dieser Wiederherstellungsprozess kann jedoch bei der Überprüfung einer Signatur nicht vollständig vertrauenswürdig sein, es sei denn, der öffentliche Schlüssel des Unterzeichners ist zuvor bekannt. Die r- und s-Werte sind die Signaturwerte, die zur Überprüfung der Signatur verwendet werden.

WICHTIG: Abhängig vom Browser-Wallet gibt die Signatur möglicherweise nur r- und s-Werte zurück. Der v-Wert wird nicht immer angegeben.

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

Das obige ist der detaillierte Inhalt vonEin Leitfaden zu Starknet-Signaturen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn