Skip to content

Commit

Permalink
Implements SNIP-12 (#637)
Browse files Browse the repository at this point in the history
* General semantic updates and EncodeType adjustment

* Improvements and bug fixes

* Implements typedData unmarshal

* Adds Message support on Unmarshal typedData

* Rename files and create types file

* Creates TestGeneral_CreateMessageWithTypes

* Clears some curve methods and adds Poseidon method

* Adds revision.go file, new Revision field of TypedData

* Basic implementation working with a rev 0 typedData

* Rename files

* Some code adjustments

* Adds new examples from starknet.js and implements new mock logic

* init function on revision package

* Adds first version of Validate feature

* Adds revision 1 support to encodeType

* Adds 'selector' support in encodeData

* restructs encodeData

* adds handleStandardTypes and handleArrays functions

* support to arrays, new types and new Domain unmarshal

* fixed support to bool, new example being tested

* fixes errors in encodeType, 'example_presetTypes' supported

* implements merkletree encode

* adds support to 'mail_StructArray' json example

* Fixes error inStringToByteArrFelt func, 'v1Nested' example passing

* Started to refactor 'encodeType' func

* Fixes bug with merkletree

* Creates the verifyType func

* implements enum encoding

* Removes the validation method, it will be added later

* creates big example for testing purpose

* removes the types file

* adds code comments and descriptions

* rename folder and file names

* creates typedData example and change READMEs

* Update utils/Felt.go

Co-authored-by: Rian Hughes <[email protected]>

* Update utils/keccak.go

Co-authored-by: Rian Hughes <[email protected]>

* addresses Rian's comment about GetMessageHash

* Revert enum wrong encode as it was fixed by starknet.js

* Creates ValidationSignature helper

* Removes the StrToFelt utility

* Improves 'chainId' validation

* Changes private fields of TypedData to public

---------

Co-authored-by: Rian Hughes <[email protected]>
  • Loading branch information
thiagodeev and rianhughes authored Dec 13, 2024
1 parent 1c399f4 commit 8ecf779
Show file tree
Hide file tree
Showing 27 changed files with 2,253 additions and 532 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ rpc*/.env.testnet

tmp/

examples/**/*.json
examples/**/*.sum

*/**/*abi.json
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ operations on the wallets. The package has excellent documentation for a smooth
- [deploy account example](./examples/deployAccount) to deploy a new account contract on testnet.
- [invoke transaction example](./examples/simpleInvoke) to add a new invoke transaction on testnet.
- [deploy contract UDC example](./examples/deployContractUDC) to deploy an ERC20 token using [UDC (Universal Deployer Contract)](https://docs.starknet.io/architecture-and-concepts/accounts/universal-deployer/) on testnet.
- [typed data example](./examples/typedData) to sign and verify a typed data.

### Run Examples

Expand Down
62 changes: 60 additions & 2 deletions curve/curve.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,19 @@ func Pedersen(a, b *felt.Felt) *felt.Felt {
return junoCrypto.Pedersen(a, b)
}

// Poseidon is a function that implements the Poseidon hash.
// NOTE: This function just wraps the Juno implementation
// (ref: https://github.com/NethermindEth/juno/blob/32fd743c774ec11a1bb2ce3dceecb57515f4873e/core/crypto/poseidon_hash.go#L59)
//
// Parameters:
// - a: a pointers to felt.Felt to be hashed.
// - b: a pointers to felt.Felt to be hashed.
// Returns:
// - *felt.Felt: a pointer to a felt.Felt storing the resulting hash.
func Poseidon(a, b *felt.Felt) *felt.Felt {
return junoCrypto.Poseidon(a, b)
}

// PedersenArray is a function that takes a variadic number of felt.Felt pointers as parameters and
// calls the PedersenArray function from the junoCrypto package with the provided parameters.
// NOTE: This function just wraps the Juno implementation
Expand All @@ -590,7 +603,7 @@ func PedersenArray(felts ...*felt.Felt) *felt.Felt {
// - felts: A variadic number of pointers to felt.Felt
// Returns:
// - *felt.Felt: pointer to a felt.Felt
func (sc StarkCurve) PoseidonArray(felts ...*felt.Felt) *felt.Felt {
func PoseidonArray(felts ...*felt.Felt) *felt.Felt {
return junoCrypto.PoseidonArray(felts...)
}

Expand All @@ -603,7 +616,7 @@ func (sc StarkCurve) PoseidonArray(felts ...*felt.Felt) *felt.Felt {
// Returns:
// - *felt.Felt: pointer to a felt.Felt
// - error: An error if any
func (sc StarkCurve) StarknetKeccak(b []byte) *felt.Felt {
func StarknetKeccak(b []byte) *felt.Felt {
return junoCrypto.StarknetKeccak(b)
}

Expand Down Expand Up @@ -709,3 +722,48 @@ func (sc StarkCurve) PrivateToPoint(privKey *big.Int) (x, y *big.Int, err error)
x, y = sc.EcMult(privKey, sc.EcGenX, sc.EcGenY)
return x, y, nil
}

// VerifySignature verifies the ECDSA signature of a given message hash using the provided public key.
//
// It takes the message hash, the r and s values of the signature, and the public key as strings and
// verifies the signature using the public key.
//
// Parameters:
// - msgHash: The hash of the message to be verified as a string
// - r: The r value (the first part) of the signature as a string
// - s: The s value (the second part) of the signature as a string
// - pubKey: The public key (only the x coordinate) as a string
// Return values:
// - bool: A boolean indicating whether the signature is valid
// - error: An error if any occurred during the verification process
func VerifySignature(msgHash, r, s, pubKey string) bool {
feltMsgHash, err := new(felt.Felt).SetString(msgHash)
if err != nil {
return false
}
feltR, err := new(felt.Felt).SetString(r)
if err != nil {
return false
}
feltS, err := new(felt.Felt).SetString(s)
if err != nil {
return false
}
pubKeyFelt, err := new(felt.Felt).SetString(pubKey)
if err != nil {
return false
}

signature := junoCrypto.Signature{
R: *feltR,
S: *feltS,
}

pubKeyStruct := junoCrypto.NewPublicKey(pubKeyFelt)
resp, err := pubKeyStruct.Verify(&signature, feltMsgHash)
if err != nil {
return false
}

return resp
}
25 changes: 25 additions & 0 deletions curve/curve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,3 +501,28 @@ func TestGeneral_SplitFactStr(t *testing.T) {
require.Equal(t, d["h"], h)
}
}

// TestGeneral_VerifySignature is a test function that verifies the correctness of the VerifySignature function.
//
// It checks if the signature of a given message hash is valid using the provided r, s values and the public key.
// The function takes no parameters and returns no values.
//
// Parameters:
// - t: The testing.T object for running the test
// Returns:
//
// none
func TestGeneral_VerifySignature(t *testing.T) {
// values verified with starknet.js

msgHash := "0x2789daed76c8b750d5a609a706481034db9dc8b63ae01f505d21e75a8fc2336"
r := "0x13e4e383af407f7ccc1f13195ff31a58cad97bbc6cf1d532798b8af616999d4"
s := "0x44dd06cf67b2ba7ea4af346d80b0b439e02a0b5893c6e4dfda9ee204211c879"
fullPubKey := "0x6c7c4408e178b2999cef9a5b3fa2a3dffc876892ad6a6bd19d1451a2256906c"

require.True(t, VerifySignature(msgHash, r, s, fullPubKey))

// Change the last digit of the message hash to test invalid signature
wrongMsgHash := "0x2789daed76c8b750d5a609a706481034db9dc8b63ae01f505d21e75a8fc2337"
require.False(t, VerifySignature(wrongMsgHash, r, s, fullPubKey))
}
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ To run an example:
R: See [simpleCall](./simpleCall/main.go).
1. How to make a function call?
R: See [simpleCall](./simpleCall/main.go).
1. How to sign and verify a typed data?
R: See [typedData](./typedData/main.go).

12 changes: 12 additions & 0 deletions examples/typedData/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
This example shows how to sign and verify a typed data.

Steps:
1. Rename the ".env.template" file located at the root of the "examples" folder to ".env"
1. Uncomment, and assign your Sepolia testnet endpoint to the `RPC_PROVIDER_URL` variable in the ".env" file
1. Uncomment, and assign your account address to the `ACCOUNT_ADDRESS` variable in the ".env" file (make sure to have a few ETH in it)
1. Uncomment, and assign your starknet public key to the `PUBLIC_KEY` variable in the ".env" file
1. Uncomment, and assign your private key to the `PRIVATE_KEY` variable in the ".env" file
1. Make sure you are in the "typedData" directory
1. Execute `go run main.go`

The message hash, signature and the verification result will be printed at the end of the execution.
35 changes: 35 additions & 0 deletions examples/typedData/baseExample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"types": {
"StarkNetDomain": [
{ "name": "name", "type": "felt" },
{ "name": "version", "type": "felt" },
{ "name": "chainId", "type": "felt" }
],
"Person": [
{ "name": "name", "type": "felt" },
{ "name": "wallet", "type": "felt" }
],
"Mail": [
{ "name": "from", "type": "Person" },
{ "name": "to", "type": "Person" },
{ "name": "contents", "type": "felt" }
]
},
"primaryType": "Mail",
"domain": {
"name": "StarkNet Mail",
"version": "1",
"chainId": 1
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}
91 changes: 91 additions & 0 deletions examples/typedData/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

import (
"context"
"encoding/json"
"fmt"
"math/big"
"os"

"github.com/NethermindEth/starknet.go/account"
"github.com/NethermindEth/starknet.go/curve"
"github.com/NethermindEth/starknet.go/rpc"
"github.com/NethermindEth/starknet.go/typedData"
"github.com/NethermindEth/starknet.go/utils"

setup "github.com/NethermindEth/starknet.go/examples/internal"
)

// NOTE : Please add in your keys only for testing purposes, in case of a leak you would potentially lose your funds.

func main() {
// Setup the account
accnt := localSetup()
fmt.Println("Account address:", accnt.AccountAddress)

// This is how you can initialize a typed data from a JSON file
var ttd typedData.TypedData
content, err := os.ReadFile("./baseExample.json")
if err != nil {
panic(fmt.Errorf("fail to read file: %w", err))
}
err = json.Unmarshal(content, &ttd)
if err != nil {
panic(fmt.Errorf("fail to unmarshal TypedData: %w", err))
}

// This is how you can get the message hash linked to your account address
messageHash, err := ttd.GetMessageHash(accnt.AccountAddress.String())
if err != nil {
panic(fmt.Errorf("fail to get message hash: %w", err))
}
fmt.Println("Message hash:", messageHash)

// This is how you can sign the message hash
signature, err := accnt.Sign(context.Background(), messageHash)
if err != nil {
panic(fmt.Errorf("fail to sign message: %w", err))
}
fmt.Println("Signature:", signature)

// This is how you can verify the signature
isValid := curve.VerifySignature(messageHash.String(), signature[0].String(), signature[1].String(), setup.GetPublicKey())
fmt.Println("Verification result:", isValid)
}

func localSetup() *account.Account {
// Load variables from '.env' file
rpcProviderUrl := setup.GetRpcProviderUrl()
accountAddress := setup.GetAccountAddress()
accountCairoVersion := setup.GetAccountCairoVersion()
privateKey := setup.GetPrivateKey()
publicKey := setup.GetPublicKey()

// Initialize connection to RPC provider
client, err := rpc.NewProvider(rpcProviderUrl)
if err != nil {
panic(fmt.Sprintf("Error dialing the RPC provider: %s", err))
}

// Initialize the account memkeyStore (set public and private keys)
ks := account.NewMemKeystore()
privKeyBI, ok := new(big.Int).SetString(privateKey, 0)
if !ok {
panic("Fail to convert privKey to bitInt")
}
ks.Put(publicKey, privKeyBI)

// Here we are converting the account address to felt
accountAddressInFelt, err := utils.HexToFelt(accountAddress)
if err != nil {
fmt.Println("Failed to transform the account address, did you give the hex address?")
panic(err)
}
// Initialize the account
accnt, err := account.NewAccount(client, accountAddressInFelt, publicKey, ks, accountCairoVersion)
if err != nil {
panic(err)
}

return accnt
}
16 changes: 8 additions & 8 deletions hash/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ func ClassHash(contract rpc.ContractClass) *felt.Felt {
ConstructorHash := hashEntryPointByType(contract.EntryPointsByType.Constructor)
ExternalHash := hashEntryPointByType(contract.EntryPointsByType.External)
L1HandleHash := hashEntryPointByType(contract.EntryPointsByType.L1Handler)
SierraProgamHash := curve.Curve.PoseidonArray(contract.SierraProgram...)
ABIHash := curve.Curve.StarknetKeccak([]byte(contract.ABI))
SierraProgamHash := curve.PoseidonArray(contract.SierraProgram...)
ABIHash := curve.StarknetKeccak([]byte(contract.ABI))

// https://docs.starknet.io/documentation/architecture_and_concepts/Network_Architecture/transactions/#deploy_account_hash_calculation
return curve.Curve.PoseidonArray(ContractClassVersionHash, ExternalHash, L1HandleHash, ConstructorHash, ABIHash, SierraProgamHash)
return curve.PoseidonArray(ContractClassVersionHash, ExternalHash, L1HandleHash, ConstructorHash, ABIHash, SierraProgamHash)
}

// hashEntryPointByType calculates the hash of an entry point by type.
Expand All @@ -83,7 +83,7 @@ func hashEntryPointByType(entryPoint []rpc.SierraEntryPoint) *felt.Felt {
for _, elt := range entryPoint {
flattened = append(flattened, elt.Selector, new(felt.Felt).SetUint64(uint64(elt.FunctionIdx)))
}
return curve.Curve.PoseidonArray(flattened...)
return curve.PoseidonArray(flattened...)
}

// CompiledClassHash calculates the hash of a compiled class in the Casm format.
Expand All @@ -97,10 +97,10 @@ func CompiledClassHash(casmClass contracts.CasmClass) *felt.Felt {
ExternalHash := hashCasmClassEntryPointByType(casmClass.EntryPointByType.External)
L1HandleHash := hashCasmClassEntryPointByType(casmClass.EntryPointByType.L1Handler)
ConstructorHash := hashCasmClassEntryPointByType(casmClass.EntryPointByType.Constructor)
ByteCodeHasH := curve.Curve.PoseidonArray(casmClass.ByteCode...)
ByteCodeHasH := curve.PoseidonArray(casmClass.ByteCode...)

// https://github.com/software-mansion/starknet.py/blob/development/starknet_py/hash/casm_class_hash.py#L10
return curve.Curve.PoseidonArray(ContractClassVersionHash, ExternalHash, L1HandleHash, ConstructorHash, ByteCodeHasH)
return curve.PoseidonArray(ContractClassVersionHash, ExternalHash, L1HandleHash, ConstructorHash, ByteCodeHasH)
}

// hashCasmClassEntryPointByType calculates the hash of a CasmClassEntryPoint array.
Expand All @@ -116,8 +116,8 @@ func hashCasmClassEntryPointByType(entryPoint []contracts.CasmClassEntryPoint) *
for _, builtIn := range elt.Builtins {
builtInFlat = append(builtInFlat, new(felt.Felt).SetBytes([]byte(builtIn)))
}
builtInHash := curve.Curve.PoseidonArray(builtInFlat...)
builtInHash := curve.PoseidonArray(builtInFlat...)
flattened = append(flattened, elt.Selector, new(felt.Felt).SetUint64(uint64(elt.Offset)), builtInHash)
}
return curve.Curve.PoseidonArray(flattened...)
return curve.PoseidonArray(flattened...)
}
Loading

0 comments on commit 8ecf779

Please sign in to comment.