From f2d3fd5385d2efd297b70013dde4a00f83d447c6 Mon Sep 17 00:00:00 2001 From: Navid TehraniFar Date: Tue, 17 Dec 2024 11:17:51 -0800 Subject: [PATCH] v1 --- examples/go.mod | 8 +- examples/go.sum | 18 +- examples/transaction_scaling/main.go | 316 +++++++++++++++++++++++++++ 3 files changed, 328 insertions(+), 14 deletions(-) create mode 100644 examples/transaction_scaling/main.go diff --git a/examples/go.mod b/examples/go.mod index 44d61f9b6..05d00fcaf 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.4 replace github.com/onflow/flow-go-sdk => ../ require ( - github.com/onflow/cadence v1.2.2 + github.com/onflow/cadence v1.3.0 github.com/onflow/flow-go-sdk v1.2.2 github.com/onflow/flowkit v1.19.0 github.com/spf13/afero v1.11.0 @@ -59,7 +59,7 @@ require ( github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect - github.com/onflow/atree v0.8.0 // indirect + github.com/onflow/atree v0.8.1 // indirect github.com/onflow/crypto v0.25.2 // indirect github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0 // indirect github.com/onflow/flow-ft/lib/go/templates v1.0.1 // indirect @@ -82,7 +82,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.15.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.9.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c // indirect github.com/turbolent/prettier v0.0.0-20220320183459-661cc755135d // indirect @@ -90,7 +90,7 @@ require ( github.com/vmihailenco/msgpack/v4 v4.3.11 // indirect github.com/vmihailenco/tagparser v0.1.1 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/zeebo/blake3 v0.2.3 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect diff --git a/examples/go.sum b/examples/go.sum index 52d307729..43ccd7dc7 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -217,7 +217,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -279,10 +278,10 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onflow/atree v0.8.0 h1:qg5c6J1gVDNObughpEeWm8oxqhPGdEyGrda121GM4u0= -github.com/onflow/atree v0.8.0/go.mod h1:yccR+LR7xc1Jdic0mrjocbHvUD7lnVvg8/Ct1AA5zBo= -github.com/onflow/cadence v1.2.2 h1:LwigF/2lPiXlwX5rFn71KeMpmW5Iu/f/JtsPLLULBCc= -github.com/onflow/cadence v1.2.2/go.mod h1:PYX1xLejqswtDsQzN93x/VpfSKNyjUk6hrkc/mpv7xs= +github.com/onflow/atree v0.8.1 h1:DAnPnL9/Ks3LaAnkQVokokTBG/znTW0DJfovDtJDhLI= +github.com/onflow/atree v0.8.1/go.mod h1:FT6udJF9Q7VQTu3wknDhFX+VV4D44ZGdqtTAE5iztck= +github.com/onflow/cadence v1.3.0 h1:COTlTqUACtTvOeFe7+jP9UDVEU3M3OZzrbzzsEbyqCk= +github.com/onflow/cadence v1.3.0/go.mod h1:638c9Zy25EwflSEE7tBFAVM9N6uwcWt77sgKpyYfSTc= github.com/onflow/crypto v0.25.2 h1:GjHunqVt+vPcdqhxxhAXiMIF3YiLX7gTuTR5O+VG2ns= github.com/onflow/crypto v0.25.2/go.mod h1:fY7eLqUdMKV8EGOw301unP8h7PvLVy8/6gVR++/g0BY= github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0 h1:u2DAG8pk0xFH7TwS70t1gSZ/FtIIZWMSNyiu4SeXBYg= @@ -384,8 +383,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/texttheater/golang-levenshtein/levenshtein v0.0.0-20200805054039-cae8b0eaed6c h1:HelZ2kAFadG0La9d+4htN4HzQ68Bm2iM9qKMSMES6xg= @@ -406,11 +405,10 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= -github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/examples/transaction_scaling/main.go b/examples/transaction_scaling/main.go new file mode 100644 index 000000000..b57a91f13 --- /dev/null +++ b/examples/transaction_scaling/main.go @@ -0,0 +1,316 @@ +/* + * Flow Go SDK + * + * Copyright Flow Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "strconv" + "sync" + "time" + + "github.com/onflow/cadence" + jsoncdc "github.com/onflow/cadence/encoding/json" + "github.com/onflow/crypto/hash" + "github.com/onflow/flow-go-sdk" + "github.com/onflow/flow-go-sdk/access/grpc" + "github.com/onflow/flow-go-sdk/crypto" + + "github.com/onflow/flow-go-sdk/examples" +) + +/* + * This program demonstrates how to run several transactions in parallel from the same account using multiple proposal keys. + * + * We are using Testnet since we want to experiment with real transaction throughput and network conditions. + * The test starts by deploying a simple counter contract and adding multiple proposal keys to the account. + * Then, it will execute several transactions in parallel, each using a different proposal key. + * Each transaction will increase the counter by 1. + * + * IMPORTANT: This demo requires a new testnet account. Use `flow keys generate` to generate a new key pair. + * Create and fund your Testnet account here: https://testnet-faucet.onflow.org/create-account + * + * Finally, set the private key and account address below before running the program. + */ + +const PRIVATE_KEY = "abc123" +const ACCOUNT_ADDRESS = "0x1234567890" + +const numProposalKeys = 420 // number of proposal keys to use, also number of workers (1 worker per key) +const numTxs = 420 // number of total transactions to execute + +const contractCode = ` + access(all) contract Counter { + + access(self) var count: Int + + init() { + self.count = 0 + } + + access(all) fun increase() { + self.count = self.count + 1 + } + + access(all) view fun getCount(): Int { + return self.count + } + } +` + +func main() { + // set up context and flow client + ctx := context.Background() + startTime := time.Now() + flowClient, err := grpc.NewClient(grpc.TestnetHost) + examples.Handle(err) + + // initialize account and signer + // this will deploy the counter contract and add the required number of proposal keys to the account + account, signer, err := InitAccount(ctx, flowClient, PRIVATE_KEY, ACCOUNT_ADDRESS, numProposalKeys) + examples.Handle(err) + + // print the current counter value + startCounterValue, err := GetCounter(ctx, flowClient, account) + examples.Handle(err) + fmt.Printf("Initial Counter: %d\n", startCounterValue) + + // populate the job channel with the number of transactions to execute + txChan := make(chan int, numTxs) + for i := 0; i < numTxs; i++ { + txChan <- i + } + + var wg sync.WaitGroup + // start the workers + for i := 0; i < numProposalKeys; i++ { + wg.Add(1) + + // worker code + // this will run in parallel for each proposal key + go func(keyIndex int) { + defer wg.Done() + + // consume the job channel + for range txChan { + fmt.Printf("[Worker %d] executing transaction\n", keyIndex) + + // execute the transaction + err := IncreaseCounter(ctx, flowClient, account, signer, keyIndex) + if err != nil { + fmt.Printf("[Worker %d] Error: %v\n", keyIndex, err) + return + } + } + }(i) + } + + close(txChan) + + // wait for all workers to finish + wg.Wait() + + finalCounter, err := GetCounter(ctx, flowClient, account) + examples.Handle(err) + fmt.Printf("Final Counter: %d\n", finalCounter) + + if finalCounter-startCounterValue != numTxs { + fmt.Printf("❌ Error: %d transactions executed, expected %d\n", numTxs, finalCounter-startCounterValue) + } else { + fmt.Printf("✅ Done! %d transactions executed in %s\n", numTxs, time.Since(startTime)) + } +} + +// Initialize the proposer keys and deploy the counter contract +func InitAccount(ctx context.Context, flowClient *grpc.Client, privateKey string, accountAddress string, numKeys int) (*flow.Account, crypto.Signer, error) { + pk, err := crypto.DecodePrivateKeyHex(crypto.ECDSA_P256, privateKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode private key %s", err) + } + account, err := flowClient.GetAccount(ctx, flow.HexToAddress(accountAddress)) + if err != nil { + return nil, nil, fmt.Errorf("failed to get account %s", err) + } + signer, err := crypto.NewInMemorySigner(pk, hash.SHA3_256) + if err != nil { + return nil, nil, fmt.Errorf("failed to create in mem signer: %s", err) + } + + if _, exists := account.Contracts["Counter"]; exists { + return nil, nil, errors.New("contract already exists") + // uncomment below to run this test on the same account again + // return account, signer, nil + } + + fmt.Println("Initializing account ...") + + initScript := ` + transaction(code: String, numKeys: Int) { + + prepare(signer: auth(AddContract, AddKey) &Account) { + // deploy the contract + signer.contracts.add(name: "Counter", code: code.decodeHex()) + + // copy the main key with 0 weight multiple times + // to create the required number of keys + let key = signer.keys.get(keyIndex: 0)! + var count: Int = 0 + while count < numKeys { + signer.keys.add( + publicKey: key.publicKey, + hashAlgorithm: key.hashAlgorithm, + weight: 0.0 + ) + count = count + 1 + } + } + } + ` + + codeHex := hex.EncodeToString([]byte(contractCode)) + + deployContractTx := flow.NewTransaction(). + SetScript([]byte(initScript)). + AddRawArgument(jsoncdc.MustEncode(cadence.String(codeHex))). + AddRawArgument(jsoncdc.MustEncode(cadence.NewInt(numKeys))). + AddAuthorizer(account.Address) + + deployContractTx.SetProposalKey( + account.Address, + account.Keys[0].Index, + account.Keys[0].SequenceNumber, + ) + + err = RunTransaction(ctx, flowClient, account, signer, deployContractTx) + if err != nil { + return nil, nil, err + } + + fmt.Println("✅ Account initialized") + + return account, signer, nil +} + +// Run a transaction and wait for it to be sealed. Note that this function does not set the proposal key. +func RunTransaction(ctx context.Context, flowClient *grpc.Client, account *flow.Account, signer crypto.Signer, tx *flow.Transaction) error { + latestBlock, err := flowClient.GetLatestBlock(ctx, true) + if err != nil { + return err + } + tx.SetReferenceBlockID(latestBlock.ID) + tx.SetPayer(account.Address) + + err = SignTransaction(ctx, flowClient, account, signer, tx) + if err != nil { + return err + } + + err = flowClient.SendTransaction(ctx, *tx) + if err != nil { + return err + } + + txRes := examples.WaitForSeal(ctx, flowClient, tx.ID()) + if txRes.Error != nil { + return txRes.Error + } + + return nil +} + +// Local signer is not thread safe, so we need to lock the mutex during the signing operation +var signingMutex sync.Mutex + +func SignTransaction(ctx context.Context, flowClient *grpc.Client, account *flow.Account, signer crypto.Signer, tx *flow.Transaction) error { + // Lock the mutex during the signing operation + signingMutex.Lock() + defer signingMutex.Unlock() + + // need an extra payload signature if the key index is not 0 + if tx.ProposalKey.KeyIndex != 0 { + err := tx.SignPayload(account.Address, tx.ProposalKey.KeyIndex, signer) + if err != nil { + return err + } + } + + // sign the envelope with the full weight key + err := tx.SignEnvelope(account.Address, account.Keys[0].Index, signer) + if err != nil { + return err + } + + return nil +} + +// Get the current counter value +func GetCounter(ctx context.Context, flowClient *grpc.Client, account *flow.Account) (int, error) { + script := []byte(fmt.Sprintf(` + import Counter from 0x%s + + access(all) fun main(): Int { + return Counter.getCount() + } + + `, account.Address.String())) + value, err := flowClient.ExecuteScriptAtLatestBlock(ctx, script, nil) + if err != nil { + return -1, err + } + + num, err := strconv.Atoi(value.String()) + if err != nil { + return -1, err + } + + return num, nil +} + +// Increase the counter by 1 by running a transaction using the given proposal key +func IncreaseCounter(ctx context.Context, flowClient *grpc.Client, account *flow.Account, signer crypto.Signer, proposalKeyIndex int) error { + script := []byte(fmt.Sprintf(` + import Counter from 0x%s + + transaction() { + prepare(signer: &Account) { + Counter.increase() + } + } + + `, account.Address.String())) + + tx := flow.NewTransaction(). + SetScript(script). + AddAuthorizer(account.Address) + + // get the latest account state including the sequence number + account, err := flowClient.GetAccount(ctx, flow.HexToAddress(account.Address.String())) + if err != nil { + return err + } + tx.SetProposalKey( + account.Address, + account.Keys[proposalKeyIndex].Index, + account.Keys[proposalKeyIndex].SequenceNumber, + ) + + return RunTransaction(ctx, flowClient, account, signer, tx) +}