From 5c4e4da43166dc2d74229a5af897a42eac88b668 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 22 Jul 2026 21:04:37 +0200 Subject: [PATCH] review fixes for localnet integration test - indexdb: byte constants for key prefixes - rosetta_handler: FLOW decimals 10^7 -> 10^8 - localnettest: exact transfer-amount assertion - Makefile: compile guard, fail-fast, jq 1.8 compat - convert_test: stray blank line --- Makefile | 17 +++----- indexdb/indexdb.go | 75 +++++++++++++++++++---------------- localnettest/localnet_test.go | 6 +++ rosetta_handler.py | 7 +++- state/convert_test.go | 1 - 5 files changed, 57 insertions(+), 49 deletions(-) diff --git a/Makefile b/Makefile index 2fb07ba..4ee12be 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,8 @@ go-build: go-test: go test -v github.com/onflow/rosetta/state/... go test -v github.com/onflow/rosetta/script/... + # Compile (but don't run) the build-tagged localnet test so it can't rot. + go test -tags localnet -run '^$$' github.com/onflow/rosetta/localnettest/... # End-to-end localnet compatibility test (script/README.md). Requires a flow-go # localnet up at 127.0.0.1:4001, the flow CLI, jq, and python3 with click + @@ -39,16 +41,9 @@ gen-originator-account: echo "Private Key: $$PRIVATE_KEY"; \ address=$$(flow accounts create --sig-algo ECDSA_secp256k1 --key $$PUBLIC_FLOW_KEY $(FLOW_CLI_FLAGS) | grep "Address" | cut -d' ' -f2 | cut -c3-);\ echo "Address created: $$address"; \ - jq --arg account_name "$(ACCOUNT_NAME)" '.accounts[$$account_name] = { \ - "address": "'$$address'", \ - "key": { \ - "type": "hex", \ - "index": 0, \ - "signatureAlgorithm": "ECDSA_secp256k1", \ - "hashAlgorithm": "SHA3_256", \ - "privateKey": "'$$PRIVATE_KEY'" \ - } \ - }' "${FLOW_JSON}" > flow.json.tmp && mv flow.json.tmp "${FLOW_JSON}" || { echo "Failed to update ${FLOW_JSON} with jq"; exit 1; }; \ + jq --arg account_name "$(ACCOUNT_NAME)" --arg address "$$address" --arg private_key "$$PRIVATE_KEY" \ + '.accounts[$$account_name] = {address: $$address, key: {type: "hex", index: 0, signatureAlgorithm: "ECDSA_secp256k1", hashAlgorithm: "SHA3_256", privateKey: $$private_key}}' \ + "${FLOW_JSON}" > flow.json.tmp && mv flow.json.tmp "${FLOW_JSON}" || { echo "Failed to update ${FLOW_JSON} with jq"; exit 1; }; \ jq --arg address "$$address" '.originators += [$$address]' "${ROSETTA_ENV}.json" > env.json.tmp && mv env.json.tmp "${ROSETTA_ENV}.json"; \ echo "$(ACCOUNT_NAME),$$KEYS,0x$$address" >> $(ACCOUNT_KEYS_FILENAME); \ echo "Updated $(FLOW_JSON), $(ROSETTA_ENV).json and $(ACCOUNT_KEYS_FILENAME)"; @@ -88,7 +83,7 @@ rosetta-transfer-funds: echo "Payer address: $$PAYER_ADDRESS"; \ RECIPIENT_ADDRESS=$$(grep '$(RECIPIENT_NAME)' $(ACCOUNT_KEYS_FILENAME) | cut -d ',' -f5); \ echo "Recipient address: $$RECIPIENT_ADDRESS"; \ - TX_HASH=$$(python3 rosetta_handler.py rosetta-transfer-funds $(ROSETTA_HOST_URL) $$PAYER_ADDRESS $$PAYER_PUBLIC_KEY $$PAYER_PRIVATE_KEY $$RECIPIENT_ADDRESS $$AMOUNT); \ + TX_HASH=$$(python3 rosetta_handler.py rosetta-transfer-funds $(ROSETTA_HOST_URL) $$PAYER_ADDRESS $$PAYER_PUBLIC_KEY $$PAYER_PRIVATE_KEY $$RECIPIENT_ADDRESS $$AMOUNT) && \ echo "Funding sent: $$TX_HASH"; # Use this target to verify that the accounts configured in the Rosetta environment JSON have the specified contracts deployed diff --git a/indexdb/indexdb.go b/indexdb/indexdb.go index 55562f5..292526b 100644 --- a/indexdb/indexdb.go +++ b/indexdb/indexdb.go @@ -25,12 +25,12 @@ var ( ErrBlockNotIndexed = errors.New("indexdb: block not indexed") ) -var ( - accountPrefix = []byte("a") - blockPrefix = []byte("b") - hash2HeightPrefix = []byte("c") - height2HashPrefix = []byte("d") - isProxyPrefix = []byte("p") +const ( + accountPrefix byte = 'a' + blockPrefix byte = 'b' + hash2HeightPrefix byte = 'c' + height2HashPrefix byte = 'd' + isProxyPrefix byte = 'p' ) // NOTE(tav): We store the blockchain data within Badger using the following @@ -88,9 +88,10 @@ func (s *Store) Accounts() (map[[8]byte]bool, error) { err := s.db.View(func(txn *badger.Txn) error { it := txn.NewIterator(badger.IteratorOptions{}) defer it.Close() - it.Seek(accountPrefix) + prefix := []byte{accountPrefix} + it.Seek(prefix) for { - if !it.ValidForPrefix(accountPrefix) { + if !it.ValidForPrefix(prefix) { break } key := it.Item().Key() @@ -101,9 +102,10 @@ func (s *Store) Accounts() (map[[8]byte]bool, error) { } it = txn.NewIterator(badger.IteratorOptions{}) defer it.Close() - it.Seek(isProxyPrefix) + prefix = []byte{isProxyPrefix} + it.Seek(prefix) for { - if !it.ValidForPrefix(isProxyPrefix) { + if !it.ValidForPrefix(prefix) { break } key := it.Item().Key() @@ -133,9 +135,10 @@ func (s *Store) AccountsInfo() (map[string]*AccountInfo, error) { err := s.db.View(func(txn *badger.Txn) error { it := txn.NewIterator(badger.IteratorOptions{}) defer it.Close() - it.Seek(accountPrefix) + prefix := []byte{accountPrefix} + it.Seek(prefix) for { - if !it.ValidForPrefix(accountPrefix) { + if !it.ValidForPrefix(prefix) { break } item := it.Item() @@ -168,9 +171,10 @@ func (s *Store) AccountsInfo() (map[string]*AccountInfo, error) { err = s.db.View(func(txn *badger.Txn) error { it := txn.NewIterator(badger.IteratorOptions{}) defer it.Close() - it.Seek(isProxyPrefix) + prefix := []byte{isProxyPrefix} + it.Seek(prefix) for { - if !it.ValidForPrefix(isProxyPrefix) { + if !it.ValidForPrefix(prefix) { break } key := it.Item().Key() @@ -312,7 +316,7 @@ func (s *Store) Genesis() *model.BlockMeta { // given height. func (s *Store) HasBalance(acct []byte, height uint64) (bool, error) { key := make([]byte, 1+8+8) - key[0] = 'a' // accountPrefix + key[0] = accountPrefix copy(key[1:9], acct) binary.BigEndian.PutUint64(key[9:], height) ok := false @@ -341,7 +345,7 @@ func (s *Store) HashForHeight(height uint64) ([]byte, error) { var hash []byte heightEnc := make([]byte, 8) binary.BigEndian.PutUint64(heightEnc, height) - key := append(height2HashPrefix, heightEnc...) + key := append([]byte{height2HashPrefix}, heightEnc...) err := s.db.View(func(txn *badger.Txn) error { item, err := txn.Get(key) if err != nil { @@ -367,7 +371,7 @@ func (s *Store) HashForHeight(height uint64) ([]byte, error) { // HeightForHash returns the block height for the given hash. func (s *Store) HeightForHash(hash []byte) (uint64, error) { height := uint64(0) - key := append(hash2HeightPrefix, hash...) + key := append([]byte{hash2HeightPrefix}, hash...) err := s.db.View(func(txn *badger.Txn) error { item, err := txn.Get(key) if err != nil { @@ -427,7 +431,7 @@ func (s *Store) Index(ctx context.Context, height uint64, hash []byte, block *mo } if len(op.ProxyPublicKey) > 0 { key := make([]byte, 17) - key[0] = 'p' // isProxyPrefix + key[0] = isProxyPrefix copy(key[1:], op.Account) binary.BigEndian.PutUint64(key[9:], height) proxyAccts = append(proxyAccts, key) @@ -444,7 +448,7 @@ func (s *Store) Index(ctx context.Context, height uint64, hash []byte, block *mo updates := make([]accountUpdate, len(accts)) for acct, diff := range accts { key := make([]byte, 1+8+8) - key[0] = 'a' // accountPrefix + key[0] = accountPrefix copy(key[1:], acct) copy(key[9:], hval) updates[i] = accountUpdate{ @@ -453,13 +457,13 @@ func (s *Store) Index(ctx context.Context, height uint64, hash []byte, block *mo } i++ } - blockKey := append(blockPrefix, hval...) + blockKey := append([]byte{blockPrefix}, hval...) blockValue, err := proto.Marshal(block) if err != nil { log.Fatalf("Failed to encode model.IndexedBlock: %s", err) } - hash2heightKey := append(hash2HeightPrefix, hash...) - height2hashKey := append(height2HashPrefix, hval...) + hash2heightKey := append([]byte{hash2HeightPrefix}, hash...) + height2hashKey := append([]byte{height2HashPrefix}, hval...) latest = &model.BlockMeta{ Hash: hash, Height: height, @@ -580,9 +584,10 @@ func (s *Store) PurgeProxyAccounts() { err := s.db.View(func(txn *badger.Txn) error { it := txn.NewIterator(badger.IteratorOptions{}) defer it.Close() - it.Seek(isProxyPrefix) + prefix := []byte{isProxyPrefix} + it.Seek(prefix) for { - if !it.ValidForPrefix(isProxyPrefix) { + if !it.ValidForPrefix(prefix) { break } key := it.Item().KeyCopy(nil) @@ -598,7 +603,7 @@ func (s *Store) PurgeProxyAccounts() { err = s.db.View(func(txn *badger.Txn) error { it := txn.NewIterator(badger.IteratorOptions{}) defer it.Close() - prefix := accountPrefix + prefix := []byte{accountPrefix} it.Seek(prefix) for { if !it.ValidForPrefix(prefix) { @@ -643,7 +648,7 @@ func (s *Store) ResetTo(base uint64) error { delKeys := [][]byte{} err := s.db.View(func(txn *badger.Txn) error { it := txn.NewIterator(badger.IteratorOptions{}) - prefix := accountPrefix + prefix := []byte{accountPrefix} it.Seek(prefix) for { if !it.ValidForPrefix(prefix) { @@ -665,7 +670,7 @@ func (s *Store) ResetTo(base uint64) error { } err = s.db.View(func(txn *badger.Txn) error { it := txn.NewIterator(badger.IteratorOptions{}) - prefix := isProxyPrefix + prefix := []byte{isProxyPrefix} it.Seek(prefix) for { if !it.ValidForPrefix(prefix) { @@ -688,7 +693,7 @@ func (s *Store) ResetTo(base uint64) error { last := uint64(0) err = s.db.View(func(txn *badger.Txn) error { it := txn.NewIterator(badger.IteratorOptions{}) - prefix := height2HashPrefix + prefix := []byte{height2HashPrefix} it.Seek(prefix) for { if !it.ValidForPrefix(prefix) { @@ -701,7 +706,7 @@ func (s *Store) ResetTo(base uint64) error { height2HashKey := item.KeyCopy(nil) delKeys = append(delKeys, height2HashKey) blockKey := make([]byte, 9) - blockKey[0] = 'b' // blockPrefix + blockKey[0] = blockPrefix binary.BigEndian.PutUint64(blockKey[1:], height) delKeys = append(delKeys, blockKey) hash, err := item.ValueCopy(nil) @@ -710,7 +715,7 @@ func (s *Store) ResetTo(base uint64) error { return err } hash2HeightKey := make([]byte, len(hash)+1) - hash2HeightKey[0] = 'c' // hash2HeightPrefix + hash2HeightKey[0] = hash2HeightPrefix copy(hash2HeightKey[1:], hash) delKeys = append(delKeys, hash2HeightKey) } else { @@ -778,15 +783,15 @@ func (s *Store) SetGenesis(val *model.BlockMeta) error { } hval := make([]byte, 8) binary.BigEndian.PutUint64(hval, val.Height) - blockKey := append(blockPrefix, hval...) + blockKey := append([]byte{blockPrefix}, hval...) blockValue, err := proto.Marshal(&model.IndexedBlock{ Timestamp: val.Timestamp, }) if err != nil { log.Fatalf("Failed to encode model.IndexedBlock: %s", err) } - hash2heightKey := append(hash2HeightPrefix, val.Hash...) - height2hashKey := append(height2HashPrefix, hval...) + hash2heightKey := append([]byte{hash2HeightPrefix}, val.Hash...) + height2hashKey := append([]byte{height2HashPrefix}, hval...) err = s.db.Update(func(txn *badger.Txn) error { if err := txn.Set([]byte("genesis"), genesis); err != nil { return err @@ -815,7 +820,7 @@ func (s *Store) SetGenesis(val *model.BlockMeta) error { func (s *Store) balanceByHeight(acct []byte, height uint64) (uint64, error) { balance := uint64(0) key := make([]byte, 1+8+8) - key[0] = 'a' + key[0] = accountPrefix copy(key[1:9], acct) binary.BigEndian.PutUint64(key[9:], height) err := s.db.View(func(txn *badger.Txn) error { @@ -844,7 +849,7 @@ func (s *Store) balanceByHeight(acct []byte, height uint64) (uint64, error) { func (s *Store) blockByHeight(height uint64) (*model.IndexedBlock, error) { block := &model.IndexedBlock{} key := make([]byte, 9) - key[0] = 'b' + key[0] = blockPrefix binary.BigEndian.PutUint64(key[1:], height) err := s.db.View(func(txn *badger.Txn) error { item, err := txn.Get(key) diff --git a/localnettest/localnet_test.go b/localnettest/localnet_test.go index 584b657..bd4a0f3 100644 --- a/localnettest/localnet_test.go +++ b/localnettest/localnet_test.go @@ -34,6 +34,9 @@ const ( originatorName = "root-originator-1" derivedName = "derived-account-1" transferAmount = "50" + // transferAmountUnits is transferAmount in Rosetta's smallest FLOW unit + // (8 decimals). + transferAmountUnits = 50 * 100_000_000 ) // indexerFatalErrors are server log lines meaning the indexer is wedged and will @@ -103,6 +106,9 @@ func TestLocalnetCompat(t *testing.T) { t.Log("waiting for the transfer to be indexed (recipient balance increases)") after := waitForBalance(t, srv, base, cfg.Network, recipient, func(v uint64) bool { return v > before }, 3*time.Minute) + if after-before != transferAmountUnits { + t.Fatalf("recipient balance rose by %d, want exactly %d", after-before, uint64(transferAmountUnits)) + } t.Logf("recipient balance %d -> %d: Rosetta indexed the transfer — compatibility confirmed", before, after) } diff --git a/rosetta_handler.py b/rosetta_handler.py index 6250b10..ffbf46a 100644 --- a/rosetta_handler.py +++ b/rosetta_handler.py @@ -151,9 +151,12 @@ def rosetta_create_derived_account(rosetta_host_url, root_originator_address, ro def rosetta_transfer_funds(rosetta_host_url, payer_address, payer_public_key, payer_private_key, recipient_address, amount, i=0): transaction = "transfer" + # FLOW amounts use 8 decimals (UFix64), matching the "decimals": 8 currency + # declared in the operations below. amount = float(amount) - amount_sent = str(-1 * int(amount * 10 ** 7)) - amount_received = str(int(amount * 10 ** 7)) + smallest_unit = int(round(amount * 10 ** 8)) + amount_sent = str(-smallest_unit) + amount_received = str(smallest_unit) operations = [ { "type": transaction, diff --git a/state/convert_test.go b/state/convert_test.go index ed595ec..2be7975 100644 --- a/state/convert_test.go +++ b/state/convert_test.go @@ -160,7 +160,6 @@ func TestDeriveEventsHash(t *testing.T) { spork := Mainnet28_SporkVersion8.create(ctx) VerifyEventsHashForSpork(t, ctx, spork, 150_000_001, 150_000_011) }) - } func VerifyEventsHashForSpork(t *testing.T, ctx context.Context, spork *config.Spork, startHeight uint64, endHeight uint64) {