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.
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.
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.
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.
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).
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)
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
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")
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:
Make sure that the message structure is the same on the frontend and backend to ensure the signature is verified correctly.
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:
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!