Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security
-->

### Added
- curve pkg:
- new `Blake2s` and `Blake2sArray` functions.
- hash pkg:
- new `CompiledClassHashV2` function to compute the compiled class hash using the Blake2s hash function.
- utils pkg:
- new `UseBlake2sHash` param in the `TxnOptions` struct to be used by the `BuildDeclareTxn` function.
- account pkg:
- new `UseBlake2sHash` param in the `TxnOptions` struct to be used by the `BuildAndSendDeclareTxn` method.

### Changed
- In the `account.BuildAndSendDeclareTxn` method, if the new `opts.UseBlake2sHash` param is nil,
Starknet.go will automatically fetch the current Starknet version and decide
whether to use the Blake2s hash function.

## [0.17.0](https://github.com/NethermindEth/starknet.go/releases/tag/v0.17.0) - 2025-11-06
### Added
- New `paymaster` pkg for interacting with paymaster services via the SNIP-29 API.
Expand Down
58 changes: 52 additions & 6 deletions account/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"time"

"github.com/Masterminds/semver/v3"
"github.com/NethermindEth/juno/core/felt"
"github.com/NethermindEth/starknet.go/client/rpcerr"
"github.com/NethermindEth/starknet.go/contracts"
Expand Down Expand Up @@ -59,8 +60,9 @@ func (account *Account) BuildAndSendInvokeTxn(
callData,
makeResourceBoundsMapWithZeroValues(),
&utils.TxnOptions{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why there is a TxnOptions defined here and another defined in the utils package?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because they're different, with different options, used by different functions.
utils.TxnOptions is used by the utils.Build... functions, while account.TxnOptions is used by the account.BuildAndSend... methods.
Even though some field names are the same, their use and documentation in the code are different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial question remains unanswered. Why there are two transaction options? Why there are two different methods to interact with transaction options?

Tip: tip,
UseQueryBit: opts.UseQueryBit,
Tip: tip,
UseQueryBit: opts.UseQueryBit,
UseBlake2sHash: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This option doesn't make sense in the context of an invoke transaction right? It shouldn't be here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linter complains about omitted fields in structs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And it makes sense, the comment is not about initializing the field or not, is about why does this field exist in the context of an invoke transaction.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because currently, the options used by all three BuildAndSend... methods are the same.

Since this is temporary, the easiest and simplest way I found was to add this new flag in the existing TxnOptions struct. Even though the "semantic" is not perfect, there's a comprehensive description. This solution brings the benefit of zero breaking changes, and at the same time, it allows high flexibility in its use.

},
)

Expand Down Expand Up @@ -138,6 +140,16 @@ func (account *Account) BuildAndSendDeclareTxn(
return response, err
}

var useBlake2sHash bool
if opts.UseBlake2sHash == nil {
useBlake2sHash, err = shouldUseBlake2sHash(ctx, account.Provider)
if err != nil {
return response, fmt.Errorf("failed to check whether to use Blake2s hash: %w", err)
}
} else {
useBlake2sHash = *opts.UseBlake2sHash
}

// building and signing the txn, as it needs a signature to estimate the fee
broadcastDeclareTxnV3, err := utils.BuildDeclareTxn(
account.Address,
Expand All @@ -146,8 +158,9 @@ func (account *Account) BuildAndSendDeclareTxn(
nonce,
makeResourceBoundsMapWithZeroValues(),
&utils.TxnOptions{
Tip: tip,
UseQueryBit: opts.UseQueryBit,
Tip: tip,
UseQueryBit: opts.UseQueryBit,
UseBlake2sHash: useBlake2sHash,
},
)
if err != nil {
Expand Down Expand Up @@ -239,8 +252,9 @@ func (account *Account) BuildAndEstimateDeployAccountTxn(
classHash,
makeResourceBoundsMapWithZeroValues(),
&utils.TxnOptions{
Tip: tip,
UseQueryBit: opts.UseQueryBit,
Tip: tip,
UseQueryBit: opts.UseQueryBit,
UseBlake2sHash: false,
},
)

Expand Down Expand Up @@ -465,3 +479,35 @@ func (account *Account) WaitForTransactionReceipt(
}
}
}

// shouldUseBlake2sHash determines whether to use the Blake2s hash function for the
// compiled class hash.
// Starknet v0.14.1 upgrade will deprecate the Poseidon hash function for the compiled class hash.
//
// Parameters:
// - ctx: The context
// - provider: The provider
//
// Returns:
// - bool: whether to use the Blake2s hash function for the compiled class hash
// - error: an error if any
func shouldUseBlake2sHash(ctx context.Context, provider rpc.RPCProvider) (bool, error) {
block, err := provider.BlockWithTxHashes(ctx, rpc.WithBlockTag(rpc.BlockTagLatest))
if err != nil {
return false, fmt.Errorf("failed to get block with tx hashes: %w", err)
}

blockTxHashes, ok := block.(*rpc.BlockTxHashes)
if !ok {
return false, fmt.Errorf("block is not a BlockTxHashes: %T", block)
}

upgradeVersion := semver.MustParse("0.14.1")

currentVersion, err := semver.NewVersion(blockTxHashes.StarknetVersion)
if err != nil {
return false, fmt.Errorf("failed to parse block's starknet version: %w", err)
}

return currentVersion.Compare(upgradeVersion) >= 0, nil
}
154 changes: 154 additions & 0 deletions account/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,155 @@ func TestBuildAndSendDeclareTxn(t *testing.T) {
assert.NotEqual(t, "0x0", txn.Transaction.(rpc.DeclareTxnV3).Tip)
}

func TestBuildAndSendDeclareTxnMock(t *testing.T) {
tests.RunTestOn(t, tests.MockEnv)

// Class
class := *internalUtils.TestUnmarshalJSONFileToType[contracts.ContractClass](
t,
"./testData/contracts_v2_HelloStarknet.sierra.json",
"",
)
// Casm Class
casmClass := *internalUtils.TestUnmarshalJSONFileToType[contracts.CasmClass](
t,
"./testData/contracts_v2_HelloStarknet.casm.json",
"",
)

t.Run("compiled class hash", func(t *testing.T) {
testcases := []struct {
name string
txnOptions *account.TxnOptions
starknetVersion string
expectedCompileClassHash string
}{
{
name: "before 0.14.1",
txnOptions: nil,
starknetVersion: "0.14.0",
expectedCompileClassHash: "0x6ff9f7df06da94198ee535f41b214dce0b8bafbdb45e6c6b09d4b3b693b1f17",
},
{
name: "before 0.14.1 + UseBlake2sHash true",
txnOptions: &account.TxnOptions{
UseBlake2sHash: &[]bool{true}[0],
},
starknetVersion: "0.14.0",
expectedCompileClassHash: "0x23c2091df2547f77185ba592b06ee2e897b0c2a70f968521a6a24fc5bfc1b1e",
},
{
name: "before 0.14.1 + UseBlake2sHash false",
txnOptions: &account.TxnOptions{
UseBlake2sHash: &[]bool{false}[0],
},
starknetVersion: "0.14.0",
expectedCompileClassHash: "0x6ff9f7df06da94198ee535f41b214dce0b8bafbdb45e6c6b09d4b3b693b1f17",
},
{
name: "after 0.14.1",
txnOptions: nil,
starknetVersion: "0.14.1",
expectedCompileClassHash: "0x23c2091df2547f77185ba592b06ee2e897b0c2a70f968521a6a24fc5bfc1b1e",
},
{
name: "after 0.14.1 + UseBlake2sHash true",
txnOptions: &account.TxnOptions{
UseBlake2sHash: &[]bool{true}[0],
},
starknetVersion: "0.14.1",
expectedCompileClassHash: "0x23c2091df2547f77185ba592b06ee2e897b0c2a70f968521a6a24fc5bfc1b1e",
},
{
name: "after 0.14.1 + UseBlake2sHash false",
txnOptions: &account.TxnOptions{
UseBlake2sHash: &[]bool{false}[0],
},
starknetVersion: "0.14.1",
expectedCompileClassHash: "0x6ff9f7df06da94198ee535f41b214dce0b8bafbdb45e6c6b09d4b3b693b1f17",
},
}

for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
mockRPCProvider := mocks.NewMockRPCProvider(ctrl)

ks, pub, _ := account.GetRandomKeys()
// called when instantiating the account
mockRPCProvider.EXPECT().ChainID(gomock.Any()).Return("SN_SEPOLIA", nil).Times(1)
acnt, err := account.NewAccount(
mockRPCProvider,
internalUtils.DeadBeef,
pub.String(),
ks,
account.CairoV2,
)
require.NoError(t, err)

// called in the BuildAndSendDeclareTxn method
mockRPCProvider.EXPECT().
Nonce(gomock.Any(), gomock.Any(), gomock.Any()).
Return(new(felt.Felt).SetUint64(1), nil).
Times(1)
mockRPCProvider.EXPECT().
BlockWithTxs(t.Context(), rpc.WithBlockTag(rpc.BlockTagLatest)).
Return(&rpc.Block{}, nil).Times(1)
mockRPCProvider.EXPECT().
EstimateFee(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return([]rpc.FeeEstimation{
{
FeeEstimationCommon: rpc.FeeEstimationCommon{
L1GasPrice: new(felt.Felt).SetUint64(10),
L1GasConsumed: new(felt.Felt).SetUint64(100),
L1DataGasPrice: new(felt.Felt).SetUint64(5),
L1DataGasConsumed: new(felt.Felt).SetUint64(50),
L2GasPrice: new(felt.Felt).SetUint64(3),
L2GasConsumed: new(felt.Felt).SetUint64(200),
},
},
}, nil).
Times(1)

var compiledClassHash *felt.Felt

if test.txnOptions == nil {
// if txnOptions is nil, the code should call the BlockWithTxHashes method to get the
// Starknet version and decide whether to use the Blake2s hash function
mockRPCProvider.EXPECT().
BlockWithTxHashes(gomock.Any(), rpc.WithBlockTag(rpc.BlockTagLatest)).
Return(&rpc.BlockTxHashes{
BlockHeader: rpc.BlockHeader{StarknetVersion: test.starknetVersion},
}, nil).Times(1)
}

mockRPCProvider.EXPECT().
AddDeclareTransaction(gomock.Any(), gomock.Any()).
DoAndReturn(
func(_, txn any) (rpc.AddDeclareTransactionResponse, error) {
declareTxn, ok := txn.(*rpc.BroadcastDeclareTxnV3)
require.True(t, ok)

compiledClassHash = declareTxn.CompiledClassHash

return rpc.AddDeclareTransactionResponse{}, nil
},
).Times(1)

_, err = acnt.BuildAndSendDeclareTxn(
t.Context(),
&casmClass,
&class,
test.txnOptions,
)
require.NoError(t, err)

assert.Equal(t, test.expectedCompileClassHash, compiledClassHash.String())
})
}
})
}

// BuildAndEstimateDeployAccountTxn is a test function that tests the BuildAndSendDeployAccount method.
//
// This function tests the BuildAndSendDeployAccount method by setting up test data and invoking the method with different test sets.
Expand Down Expand Up @@ -411,6 +560,11 @@ func TestBuildAndSendMethodsWithQueryBit(t *testing.T) {
})

t.Run("BuildAndSendDeclareTxn", func(t *testing.T) {
mockRPCProvider.EXPECT().
BlockWithTxHashes(gomock.Any(), rpc.WithBlockTag(rpc.BlockTagLatest)).
Return(&rpc.BlockTxHashes{
BlockHeader: rpc.BlockHeader{StarknetVersion: "0.14.1"},
}, nil).Times(1)
mockRPCProvider.EXPECT().AddDeclareTransaction(gomock.Any(), gomock.Any()).DoAndReturn(
func(_, txn any) (rpc.AddDeclareTransactionResponse, error) {
bcTxn, ok := txn.(*rpc.BroadcastDeclareTxnV3)
Expand Down
8 changes: 8 additions & 0 deletions account/txn_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ type TxnOptions struct {
UseLatest bool
// The simulation flag to be used when estimating fees. Default: none.
SimulationFlag rpc.SimulationFlag

// ONLY FOR THE `BuildAndSendDeclareTxn` METHOD: A pointer to a boolean flag
// indicating whether to use the Blake2s hash function to calculate the compiled
// class hash.
// If omitted (nil), Starknet.go will automatically fetch the current Starknet
// version and decide whether to use the Blake2s hash function.
UseBlake2sHash *bool
// TODO: remove this field after the Starknet v0.14.1 upgrade
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intention is to represent three states, let's use an enum. Which will be hash:

  • "Auto"
  • "Blake2s"
  • "Poseidon"

I think it is more descriptive code

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked the idea, but since this is temporary (will be removed after the v0.14.1 mainnet upgrade), what about leaving it as is? Less code, and it will be simple to implement in older starknet.go versions as I will need to create another release for rpcv08 users

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with this logic. Let's revise this.

Is this code only used for invoking transactions / hence it only mean they work on the tip of the chain?

Right?

}

// BlockID returns the block ID for fee estimation based on the UseLatest flag.
Expand Down
52 changes: 47 additions & 5 deletions curve/curve.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"math/big"

junoCrypto "github.com/NethermindEth/juno/core/crypto"
"github.com/NethermindEth/juno/core/crypto/blake2s"
"github.com/NethermindEth/juno/core/felt"
internalUtils "github.com/NethermindEth/starknet.go/internal/utils"
starkcurve "github.com/consensys/gnark-crypto/ecc/stark-curve"
Expand Down Expand Up @@ -243,7 +244,9 @@ func ComputeHashOnElements(elems []*big.Int) (hash *big.Int) {
// Returns:
// - *felt.Felt: a pointer to a felt.Felt storing the resulting hash.
func Pedersen(a, b *felt.Felt) *felt.Felt {
return junoCrypto.Pedersen(a, b)
hash := junoCrypto.Pedersen(a, b)

return &hash
}

// Poseidon is a function that implements the Poseidon hash.
Expand All @@ -255,7 +258,24 @@ func Pedersen(a, b *felt.Felt) *felt.Felt {
// 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)
hash := junoCrypto.Poseidon(a, b)

return &hash
}

// Blake2s is a function that implements the Blake2s hash.
//
// 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 Blake2s(a, b *felt.Felt) *felt.Felt {
hash := blake2s.Blake2s(a, b)
hashFelt := felt.Felt(hash)

return &hashFelt
}

// PedersenArray is a function that takes a variadic number of felt.Felt
Expand All @@ -268,7 +288,9 @@ func Poseidon(a, b *felt.Felt) *felt.Felt {
// Returns:
// - *felt.Felt: pointer to a felt.Felt
func PedersenArray(felts ...*felt.Felt) *felt.Felt {
return junoCrypto.PedersenArray(felts...)
hash := junoCrypto.PedersenArray(felts...)

return &hash
}

// PoseidonArray is a function that takes a variadic number of felt.Felt
Expand All @@ -281,7 +303,25 @@ func PedersenArray(felts ...*felt.Felt) *felt.Felt {
// Returns:
// - *felt.Felt: pointer to a felt.Felt
func PoseidonArray(felts ...*felt.Felt) *felt.Felt {
return junoCrypto.PoseidonArray(felts...)
hash := junoCrypto.PoseidonArray(felts...)

return &hash
}

// Blake2sArray is a function that takes a variadic number of felt.Felt
// pointers as parameters and calls the Blake2sArray function from the
// junoCrypto package with the provided parameters.
//
// Parameters:
// - felts: A variadic number of pointers to felt.Felt
//
// Returns:
// - *felt.Felt: pointer to a felt.Felt
func Blake2sArray(felts ...*felt.Felt) *felt.Felt {
hash := blake2s.Blake2sArray(felts...)
hashFelt := felt.Felt(hash)

return &hashFelt
}

// StarknetKeccak computes the Starknet Keccak hash of the given byte slice.
Expand All @@ -293,7 +333,9 @@ func PoseidonArray(felts ...*felt.Felt) *felt.Felt {
// - *felt.Felt: pointer to a felt.Felt
// - error: An error if any
func StarknetKeccak(b []byte) *felt.Felt {
return junoCrypto.StarknetKeccak(b)
hash := junoCrypto.StarknetKeccak(b)

return &hash
}

// fmtPrivKey formats a private key to a 32 bytes array by padding it
Expand Down
Loading
Loading