From b7edfabb4807fb6ce1e600d889490632e72f01d7 Mon Sep 17 00:00:00 2001 From: gagliardetto Date: Wed, 8 Nov 2023 18:14:16 +0100 Subject: [PATCH 1/9] Fix error checking --- cmd-x-index-all.go | 5 +++-- gsfa/manifest/manifest.go | 3 ++- index-cid-to-offset.go | 2 +- readahead/readahead.go | 3 ++- readers.go | 6 +++--- store/index/gc.go | 5 +++-- store/index/index.go | 9 +++++---- store/index/upgrade.go | 3 ++- store/index/upgrade_test.go | 4 ++-- store/primary/gsfaprimary/gsfaprimary.go | 2 +- store/primary/gsfaprimary/upgrade_test.go | 4 ++-- store/primary/sig2epochprimary/upgrade_test.go | 4 ++-- store/store_test.go | 3 ++- 13 files changed, 30 insertions(+), 23 deletions(-) diff --git a/cmd-x-index-all.go b/cmd-x-index-all.go index db06ffdf..1eae0589 100644 --- a/cmd-x-index-all.go +++ b/cmd-x-index-all.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "errors" "fmt" "io" "math/rand" @@ -211,7 +212,7 @@ func createAllIndexes( for { _cid, sectionLength, block, err := rd.NextNode() if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return nil, err @@ -611,7 +612,7 @@ func verifyAllIndexes( for { _cid, sectionLength, block, err := rd.NextNode() if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return err diff --git a/gsfa/manifest/manifest.go b/gsfa/manifest/manifest.go index 1cc71516..4f6a7799 100644 --- a/gsfa/manifest/manifest.go +++ b/gsfa/manifest/manifest.go @@ -2,6 +2,7 @@ package manifest import ( "encoding/binary" + "errors" "fmt" "io" "os" @@ -213,7 +214,7 @@ func (m *Manifest) readAllContent() (Values, error) { values := make([][2]uint64, 0, currentContentSize/16) for { _, err := io.ReadFull(sectionReader, buf) - if err == io.EOF { + if errors.Is(err, io.EOF) { break } if err != nil { diff --git a/index-cid-to-offset.go b/index-cid-to-offset.go index ceae6a0e..2907f1db 100644 --- a/index-cid-to-offset.go +++ b/index-cid-to-offset.go @@ -89,7 +89,7 @@ func CreateIndex_cid2offset(ctx context.Context, tmpDir string, carPath string, for { c, sectionLength, err := rd.NextInfo() if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return "", err diff --git a/readahead/readahead.go b/readahead/readahead.go index a804bb71..124514b0 100644 --- a/readahead/readahead.go +++ b/readahead/readahead.go @@ -2,6 +2,7 @@ package readahead import ( "bytes" + "errors" "fmt" "io" "os" @@ -78,7 +79,7 @@ func (cr *CachingReader) Read(p []byte) (int, error) { if n > 0 { cr.buffer.Write(tmp[:n]) } - if err == io.EOF && cr.buffer.Len() == 0 { + if errors.Is(err, io.EOF) && cr.buffer.Len() == 0 { // If EOF is reached and buffer is empty, return EOF return 0, io.EOF } diff --git a/readers.go b/readers.go index 5fb53e1e..f5770033 100644 --- a/readers.go +++ b/readers.go @@ -172,7 +172,7 @@ func isDirEmpty(dir string) (bool, error) { defer file.Close() _, err = file.Readdir(1) - if err == io.EOF { + if errors.Is(err, io.EOF) { return true, nil } return false, err @@ -202,7 +202,7 @@ func carCountItems(carPath string) (uint64, error) { for { _, _, err := rd.NextInfo() if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return 0, err @@ -230,7 +230,7 @@ func carCountItemsByFirstByte(carPath string) (map[byte]uint64, error) { for { _, _, block, err := rd.NextNode() if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return nil, err diff --git a/store/index/gc.go b/store/index/gc.go index d9e6c8b2..ebfadde5 100644 --- a/store/index/gc.go +++ b/store/index/gc.go @@ -8,6 +8,7 @@ package index import ( "context" "encoding/binary" + "errors" "fmt" "io" "os" @@ -308,7 +309,7 @@ func (index *Index) reapIndexRecords(ctx context.Context, fileNum uint32, indexP return false, ctx.Err() } if _, err = file.ReadAt(sizeBuf, pos); err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { // Finished reading entire index. break } @@ -348,7 +349,7 @@ func (index *Index) reapIndexRecords(ctx context.Context, fileNum uint32, indexP } data := scratch[:size] if _, err = file.ReadAt(data, pos+sizePrefixSize); err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { // The data has not been written yet, or the file is corrupt. // Take the data we are able to use and move on. break diff --git a/store/index/index.go b/store/index/index.go index 83a68779..66015daa 100644 --- a/store/index/index.go +++ b/store/index/index.go @@ -10,6 +10,7 @@ import ( "bytes" "context" "encoding/binary" + "errors" "fmt" "io" "os" @@ -362,7 +363,7 @@ func scanIndexFile(ctx context.Context, basePath string, fileNum uint32, buckets var i int for { if _, err = file.ReadAt(sizeBuffer, pos); err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { // Finished reading entire index. break } @@ -392,7 +393,7 @@ func scanIndexFile(ctx context.Context, basePath string, fileNum uint32, buckets } data := scratch[:size] if _, err = file.ReadAt(data, pos); err != nil { - if err == io.ErrUnexpectedEOF || err == io.EOF { + if err == io.ErrUnexpectedEOF || errors.Is(err, io.EOF) { // The file is corrupt since the expected data could not be // read. Take the usable data and move on. log.Errorw("Unexpected EOF scanning index record", "file", indexPath) @@ -1059,7 +1060,7 @@ func (iter *RawIterator) Next() ([]byte, types.Position, bool, error) { _, err := iter.file.ReadAt(sizeBuf, iter.pos) if err != nil { iter.file.Close() - if err == io.EOF { + if errors.Is(err, io.EOF) { iter.file = nil iter.fileNum++ return iter.Next() @@ -1488,7 +1489,7 @@ func MoveFiles(indexPath, newDir string) error { for { fileName, err := fileIter.next() if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return err diff --git a/store/index/upgrade.go b/store/index/upgrade.go index 5d6ce441..09a3b2ab 100644 --- a/store/index/upgrade.go +++ b/store/index/upgrade.go @@ -9,6 +9,7 @@ import ( "bufio" "context" "encoding/binary" + "errors" "fmt" "io" "os" @@ -91,7 +92,7 @@ func chunkOldIndex(ctx context.Context, file *os.File, name string, fileSizeLimi for { _, err = io.ReadFull(reader, sizeBuffer) if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return 0, err diff --git a/store/index/upgrade_test.go b/store/index/upgrade_test.go index b00baf36..30846c9d 100644 --- a/store/index/upgrade_test.go +++ b/store/index/upgrade_test.go @@ -104,7 +104,7 @@ func testScanIndexFile(file *os.File, fileNum uint32, buckets Buckets, prevSize for { _, err := io.ReadFull(buffered, sizeBuffer) if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return err @@ -119,7 +119,7 @@ func testScanIndexFile(file *os.File, fileNum uint32, buckets Buckets, prevSize data := scratch[:size] _, err = io.ReadFull(buffered, data) if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { return errors.New("unexpected EOF") } return err diff --git a/store/primary/gsfaprimary/gsfaprimary.go b/store/primary/gsfaprimary/gsfaprimary.go index 5779d404..d0463422 100644 --- a/store/primary/gsfaprimary/gsfaprimary.go +++ b/store/primary/gsfaprimary/gsfaprimary.go @@ -496,7 +496,7 @@ func (iter *Iterator) Next() ([]byte, []byte, error) { _, err := iter.file.ReadAt(data, pos) if err != nil { iter.file.Close() - // if err == io.EOF { + // if errors.Is(err, io.EOF) { // err = io.ErrUnexpectedEOF // } return nil, nil, err diff --git a/store/primary/gsfaprimary/upgrade_test.go b/store/primary/gsfaprimary/upgrade_test.go index 0b99202b..f8626cbc 100644 --- a/store/primary/gsfaprimary/upgrade_test.go +++ b/store/primary/gsfaprimary/upgrade_test.go @@ -130,7 +130,7 @@ func testScanPrimaryFile(file *os.File) ([][]byte, error) { for { _, err := io.ReadFull(buffered, sizeBuffer) if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return nil, err @@ -143,7 +143,7 @@ func testScanPrimaryFile(file *os.File) ([][]byte, error) { data := scratch[:size] _, err = io.ReadFull(buffered, data) if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { return nil, errors.New("unexpected EOF") } return nil, err diff --git a/store/primary/sig2epochprimary/upgrade_test.go b/store/primary/sig2epochprimary/upgrade_test.go index 4ea0bdfe..ad33b55c 100644 --- a/store/primary/sig2epochprimary/upgrade_test.go +++ b/store/primary/sig2epochprimary/upgrade_test.go @@ -130,7 +130,7 @@ func testScanPrimaryFile(file *os.File) ([][]byte, error) { for { _, err := io.ReadFull(buffered, sizeBuffer) if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { break } return nil, err @@ -143,7 +143,7 @@ func testScanPrimaryFile(file *os.File) ([][]byte, error) { data := scratch[:size] _, err = io.ReadFull(buffered, data) if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { return nil, errors.New("unexpected EOF") } return nil, err diff --git a/store/store_test.go b/store/store_test.go index faba3c2d..9591150b 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -2,6 +2,7 @@ package store_test import ( "context" + "errors" "io" "os" "path/filepath" @@ -89,7 +90,7 @@ func TestUpdate(t *testing.T) { var count int for { key, val, err := storeIter.Next() - if err == io.EOF { + if errors.Is(err, io.EOF) { break } require.Zero(t, count) From 1b8135df3a0556f5eafb6f933622e2455ff786d2 Mon Sep 17 00:00:00 2001 From: gagliardetto Date: Wed, 8 Nov 2023 18:14:36 +0100 Subject: [PATCH 2/9] Handle block zero for mainnet --- multiepoch-getBlock.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/multiepoch-getBlock.go b/multiepoch-getBlock.go index d408a055..30d97b22 100644 --- a/multiepoch-getBlock.go +++ b/multiepoch-getBlock.go @@ -69,6 +69,9 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex tim.time("GetBlock") { prefetcherFromCar := func() error { + if slot == 0 { + return nil + } parentIsInPreviousEpoch := CalcEpochForSlot(uint64(block.Meta.Parent_slot)) != CalcEpochForSlot(slot) var blockCid, parentCid cid.Cid @@ -398,6 +401,25 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex blockResp.Blockhash = lastEntryHash.String() blockResp.ParentSlot = uint64(block.Meta.Parent_slot) blockResp.Rewards = rewards + if slot == 0 { + // NOTE: we assume this is on mainnet. + blockZeroBlocktime := uint64(1584368940) + zeroBlockHeight := uint64(0) + blockZeroBlockHash := lastEntryHash.String() + var blockResp GetBlockResponse + blockResp.Transactions = make([]GetTransactionResponse, 0) + blockResp.BlockTime = &blockZeroBlocktime + blockResp.Blockhash = lastEntryHash.String() + blockResp.ParentSlot = uint64(0) + blockResp.Rewards = make([]any, 0) + blockResp.BlockHeight = &zeroBlockHeight + blockResp.PreviousBlockhash = &blockZeroBlockHash // NOTE: this is what solana RPC does. Should it be nil instead? Or should it be the genesis hash? + return nil, conn.ReplyRaw( + ctx, + req.ID, + blockResp, + ) + } { blockHeight, ok := block.GetBlockHeight() From 8bb43251fb716280dc1036b5b079ca9d2fc0cdce Mon Sep 17 00:00:00 2001 From: gagliardetto Date: Wed, 8 Nov 2023 19:02:13 +0100 Subject: [PATCH 3/9] Fix slot 1 --- multiepoch-getBlock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multiepoch-getBlock.go b/multiepoch-getBlock.go index 30d97b22..01ba243d 100644 --- a/multiepoch-getBlock.go +++ b/multiepoch-getBlock.go @@ -430,7 +430,7 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex { // get parent slot parentSlot := uint64(block.Meta.Parent_slot) - if parentSlot != 0 && CalcEpochForSlot(parentSlot) == epochNumber { + if (parentSlot != 0 || slot == 1) && CalcEpochForSlot(parentSlot) == epochNumber { // NOTE: if the parent is in the same epoch, we can get it from the same epoch handler as the block; // otherwise, we need to get it from the previous epoch (TODO: implement this) parentBlock, err := epochHandler.GetBlock(WithSubrapghPrefetch(ctx, false), parentSlot) From 372c4567841c22c39045e3cc4047c17ad58b09b4 Mon Sep 17 00:00:00 2001 From: gagliardetto Date: Wed, 8 Nov 2023 22:01:36 +0100 Subject: [PATCH 4/9] Check sooner if it's block zero --- multiepoch-getBlock.go | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/multiepoch-getBlock.go b/multiepoch-getBlock.go index 01ba243d..14686c59 100644 --- a/multiepoch-getBlock.go +++ b/multiepoch-getBlock.go @@ -249,6 +249,26 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex } tim.time("get entries") + if slot == 0 { + // NOTE: we assume this is on mainnet. + blockZeroBlocktime := uint64(1584368940) + zeroBlockHeight := uint64(0) + blockZeroBlockHash := lastEntryHash.String() + var blockResp GetBlockResponse + blockResp.Transactions = make([]GetTransactionResponse, 0) + blockResp.BlockTime = &blockZeroBlocktime + blockResp.Blockhash = lastEntryHash.String() + blockResp.ParentSlot = uint64(0) + blockResp.Rewards = make([]any, 0) + blockResp.BlockHeight = &zeroBlockHeight + blockResp.PreviousBlockhash = &blockZeroBlockHash // NOTE: this is what solana RPC does. Should it be nil instead? Or should it be the genesis hash? + return nil, conn.ReplyRaw( + ctx, + req.ID, + blockResp, + ) + } + var allTransactions []GetTransactionResponse var rewards any hasRewards := !block.Rewards.(cidlink.Link).Cid.Equals(DummyCID) @@ -391,6 +411,7 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex allTransactions = append(allTransactions, txResp) } } + sort.Slice(allTransactions, func(i, j int) bool { return allTransactions[i].Position < allTransactions[j].Position }) @@ -401,25 +422,6 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex blockResp.Blockhash = lastEntryHash.String() blockResp.ParentSlot = uint64(block.Meta.Parent_slot) blockResp.Rewards = rewards - if slot == 0 { - // NOTE: we assume this is on mainnet. - blockZeroBlocktime := uint64(1584368940) - zeroBlockHeight := uint64(0) - blockZeroBlockHash := lastEntryHash.String() - var blockResp GetBlockResponse - blockResp.Transactions = make([]GetTransactionResponse, 0) - blockResp.BlockTime = &blockZeroBlocktime - blockResp.Blockhash = lastEntryHash.String() - blockResp.ParentSlot = uint64(0) - blockResp.Rewards = make([]any, 0) - blockResp.BlockHeight = &zeroBlockHeight - blockResp.PreviousBlockhash = &blockZeroBlockHash // NOTE: this is what solana RPC does. Should it be nil instead? Or should it be the genesis hash? - return nil, conn.ReplyRaw( - ctx, - req.ID, - blockResp, - ) - } { blockHeight, ok := block.GetBlockHeight() From 4d4213f403821389dd12f4885e749bed395ac773 Mon Sep 17 00:00:00 2001 From: gagliardetto Date: Wed, 15 Nov 2023 15:59:42 +0100 Subject: [PATCH 5/9] Include genesis reading/parsing code from https://github.com/firedancer-io/radiance --- radiance/LICENSE | 202 ++++++++++++++++++ radiance/archiveutil/archiveutil.go | 56 +++++ radiance/genesis/file.go | 72 +++++++ radiance/genesis/file_test.go | 67 ++++++ radiance/genesis/genesis.go | 38 ++++ radiance/genesis/serde.go | 90 ++++++++ .../genesis/testdata/mainnet/genesis.tar.bz2 | Bin 0 -> 34256 bytes radiance/runtime/runtime.go | 75 +++++++ radiance/runtime/serde.go | 118 ++++++++++ 9 files changed, 718 insertions(+) create mode 100644 radiance/LICENSE create mode 100644 radiance/archiveutil/archiveutil.go create mode 100644 radiance/genesis/file.go create mode 100644 radiance/genesis/file_test.go create mode 100644 radiance/genesis/genesis.go create mode 100644 radiance/genesis/serde.go create mode 100644 radiance/genesis/testdata/mainnet/genesis.tar.bz2 create mode 100644 radiance/runtime/runtime.go create mode 100644 radiance/runtime/serde.go diff --git a/radiance/LICENSE b/radiance/LICENSE new file mode 100644 index 00000000..c817b165 --- /dev/null +++ b/radiance/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Firedancer Contributors + + 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. diff --git a/radiance/archiveutil/archiveutil.go b/radiance/archiveutil/archiveutil.go new file mode 100644 index 00000000..201395ba --- /dev/null +++ b/radiance/archiveutil/archiveutil.go @@ -0,0 +1,56 @@ +// Package archiveutil helps dealing with common archive file formats. +package archiveutil + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/bzip2" + "compress/gzip" + "fmt" + "io" +) + +// TODO: zstd support +// TODO: xz support + +// OpenTar opens a `.tar`, `.tar.gz`, or `.tar.bz2` file. +// +// Peeks the first few bytes in the given reader and auto-detects the file format. +// Returns a tar reader spliced together with a decompressor if necessary. +func OpenTar(rawRd io.Reader) (*tar.Reader, error) { + rd := bufio.NewReader(rawRd) + magicBytes, err := rd.Peek(6) + if err != nil { + return nil, fmt.Errorf("failed to detect magic: %w", err) + } + uncompressedRd := io.Reader(rd) + + // Check first few bytes for known compression magics. + if bytes.Equal(magicBytes[:2], []byte("BZ")) { + uncompressedRd = bzip2.NewReader(rd) + } else if bytes.Equal(magicBytes[:3], []byte{0x1f, 0x8b, 0x08}) { + uncompressedRd, err = gzip.NewReader(rd) + if err != nil { + return nil, fmt.Errorf("invalid .tar.gz: %w", err) + } + } else if bytes.Equal(magicBytes[:6], []byte{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00}) { + return nil, fmt.Errorf(".tar.xz not supported yet") + } else if bytes.Equal(magicBytes[1:4], []byte{0xb5, 0x2f, 0xfd}) { + return nil, fmt.Errorf(".tar.zst not supported yet") + } else { + // Presumed uncompressed case. + // Peek and see if we can find a valid tar header. + peek, err := rd.Peek(1024) + if err != nil { + return nil, err + } + peekTar := tar.NewReader(bytes.NewReader(peek)) + if _, err = peekTar.Next(); err != nil { + // Doesn't seem to be a valid tar header, bail. + return nil, fmt.Errorf("unknown archive format") + } + } + + return tar.NewReader(uncompressedRd), nil +} diff --git a/radiance/genesis/file.go b/radiance/genesis/file.go new file mode 100644 index 00000000..2c4d411f --- /dev/null +++ b/radiance/genesis/file.go @@ -0,0 +1,72 @@ +package genesis + +import ( + "archive/tar" + "crypto/sha256" + "fmt" + "io" + "os" + + bin "github.com/gagliardetto/binary" + "github.com/rpcpool/yellowstone-faithful/radiance/archiveutil" +) + +// ReadGenesisFromFile is a convenience wrapper for ReadGenesisFromArchive. +func ReadGenesisFromFile(fpath string) (genesis *Genesis, hash *[32]byte, err error) { + f, err := os.Open(fpath) + if err != nil { + return nil, nil, err + } + defer f.Close() + return ReadGenesisFromArchive(f) +} + +// ReadGenesisFromArchive reads a `genesis.tar.bz2` file. +func ReadGenesisFromArchive(archive io.Reader) (genesis *Genesis, hash *[32]byte, err error) { + var files *tar.Reader + var hdr *tar.Header + files, err = archiveutil.OpenTar(archive) + if err != nil { + return + } + hdr, err = files.Next() + if err != nil { + return + } + if hdr.Name != "genesis.bin" { + err = fmt.Errorf("first file is not genesis.bin") + return + } + + // Read and hash first file + const maxSize = 10_000_001 + var rd io.Reader + rd = files + hasher := sha256.New() + rd = io.TeeReader(rd, hasher) + rd = io.LimitReader(rd, maxSize) + + // Decode content + var genesisBytes []byte + genesisBytes, err = io.ReadAll(rd) + if err != nil { + return + } + if len(genesisBytes) >= maxSize { + err = fmt.Errorf("genesis.bin too large") + return + } + genesis = new(Genesis) + dec := bin.NewBinDecoder(genesisBytes) + err = dec.Decode(genesis) + if err == nil { + if dec.HasRemaining() { + err = fmt.Errorf("not all of genesis.bin was read (%d bytes remaining)", dec.Remaining()) + } else { + hash = new([32]byte) + hasher.Sum(hash[:0]) + } + } + + return +} diff --git a/radiance/genesis/file_test.go b/radiance/genesis/file_test.go new file mode 100644 index 00000000..3a9f618c --- /dev/null +++ b/radiance/genesis/file_test.go @@ -0,0 +1,67 @@ +package genesis + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/rpcpool/yellowstone-faithful/radiance/runtime" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadGenesisFromArchive(t *testing.T) { + f, err := os.Open(filepath.Join("testdata", "mainnet", "genesis.tar.bz2")) + require.NoError(t, err) + defer f.Close() + genesis, _, err := ReadGenesisFromArchive(f) + require.NoError(t, err) + + assert.Equal(t, time.Date(2020, time.March, 16, 14, 29, 0, 0, time.UTC), genesis.CreationTime) + assert.Equal(t, int64(1584368940), genesis.CreationTime.Unix()) + assert.Equal(t, []BuiltinProgram{ + { + Key: "solana_config_program", + Pubkey: solana.MustPublicKeyFromBase58("Config1111111111111111111111111111111111111"), + }, + { + Key: "solana_stake_program", + Pubkey: solana.MustPublicKeyFromBase58("Stake11111111111111111111111111111111111111"), + }, + { + Key: "solana_system_program", + Pubkey: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"), + }, + { + Key: "solana_vote_program", + Pubkey: solana.MustPublicKeyFromBase58("Vote111111111111111111111111111111111111111"), + }, + }, genesis.Builtins) + assert.Equal(t, uint64(0x40), genesis.TicksPerSlot) + assert.Equal(t, runtime.PohParams{ + TickDuration: 6250000, + HasHashesPerTick: true, + HashesPerTick: 12500, + HasTickCount: false, + }, genesis.PohParams) + assert.Equal(t, runtime.FeeParams{ + TargetLamportsPerSig: 10000, + TargetSigsPerSlot: 20000, + MinLamportsPerSig: 5000, + MaxLamportsPerSig: 100000, + BurnPercent: 100, + }, genesis.Fees) + assert.Equal(t, runtime.RentParams{ + LamportsPerByteYear: 3480, + ExemptionThreshold: 2, + BurnPercent: 100, + }, genesis.Rent) + assert.Equal(t, runtime.InflationParams{ /* empty */ }, genesis.Inflation) + assert.Equal(t, runtime.EpochSchedule{ + SlotPerEpoch: 432000, + LeaderScheduleSlotOffset: 432000, + }, genesis.EpochSchedule) + assert.Equal(t, uint32(1), genesis.ClusterID) +} diff --git a/radiance/genesis/genesis.go b/radiance/genesis/genesis.go new file mode 100644 index 00000000..a74bac67 --- /dev/null +++ b/radiance/genesis/genesis.go @@ -0,0 +1,38 @@ +package genesis + +import ( + "time" + + "github.com/rpcpool/yellowstone-faithful/radiance/runtime" +) + +// Genesis contains the genesis state of a Solana ledger. +type Genesis struct { + CreationTime time.Time + Accounts []AccountEntry + Builtins []BuiltinProgram + RewardPools []AccountEntry + TicksPerSlot uint64 + PohParams runtime.PohParams + Fees runtime.FeeParams + Rent runtime.RentParams + Inflation runtime.InflationParams + EpochSchedule runtime.EpochSchedule + ClusterID uint32 +} + +type AccountEntry struct { + Pubkey [32]byte + runtime.Account +} + +type BuiltinProgram struct { + Key string + Pubkey [32]byte +} + +func (g *Genesis) FillAccounts(state runtime.Accounts) { + for _, acc := range g.Accounts { + state.SetAccount(&acc.Pubkey, &acc.Account) + } +} diff --git a/radiance/genesis/serde.go b/radiance/genesis/serde.go new file mode 100644 index 00000000..b28808e9 --- /dev/null +++ b/radiance/genesis/serde.go @@ -0,0 +1,90 @@ +package genesis + +import ( + "io" + "time" + + bin "github.com/gagliardetto/binary" + "github.com/rpcpool/yellowstone-faithful/radiance/runtime" +) + +// Dumping ground for handwritten serialization boilerplate. +// To be removed when switching over to serde-generate. + +func (g *Genesis) UnmarshalWithDecoder(decoder *bin.Decoder) (err error) { + var raw struct { + CreationTime int64 + NumAccounts uint64 `bin:"sizeof=Accounts"` + Accounts []AccountEntry + NumBuiltins uint64 `bin:"sizeof=Builtins"` + Builtins []BuiltinProgram + NumRewardPools uint64 `bin:"sizeof=RewardPools"` + RewardPools []AccountEntry + TicksPerSlot uint64 + Padding00 uint64 + PohParams runtime.PohParams + Padding01 uint64 + Fees runtime.FeeParams + Rent runtime.RentParams + Inflation runtime.InflationParams + EpochSchedule runtime.EpochSchedule + ClusterID uint32 + } + if err = decoder.Decode(&raw); err != nil { + return err + } + *g = Genesis{ + CreationTime: time.Unix(raw.CreationTime, 0).UTC(), + Accounts: raw.Accounts, + Builtins: raw.Builtins, + RewardPools: raw.RewardPools, + TicksPerSlot: raw.TicksPerSlot, + PohParams: raw.PohParams, + Fees: raw.Fees, + Rent: raw.Rent, + Inflation: raw.Inflation, + EpochSchedule: raw.EpochSchedule, + ClusterID: raw.ClusterID, + } + return nil +} + +func (g *Genesis) MarshalWithEncoder(_ *bin.Encoder) (err error) { + // TODO not implemented + panic("not implemented") +} + +func (a *AccountEntry) UnmarshalWithDecoder(decoder *bin.Decoder) (err error) { + if err = decoder.Decode(&a.Pubkey); err != nil { + return err + } + return a.Account.UnmarshalWithDecoder(decoder) +} + +func (a *AccountEntry) MarshalWihEncoder(encoder *bin.Encoder) (err error) { + if err = encoder.WriteBytes(a.Pubkey[:], false); err != nil { + return err + } + return a.Account.MarshalWihEncoder(encoder) +} + +func (b *BuiltinProgram) UnmarshalWithDecoder(decoder *bin.Decoder) (err error) { + var strLen uint64 + if strLen, err = decoder.ReadUint64(bin.LE); err != nil { + return err + } + if strLen > uint64(decoder.Remaining()) { + return io.ErrUnexpectedEOF + } + var strBytes []byte + if strBytes, err = decoder.ReadNBytes(int(strLen)); err != nil { + return err + } + b.Key = string(strBytes) + return decoder.Decode(&b.Pubkey) +} + +func (*BuiltinProgram) MarshalWihEncoder(_ *bin.Encoder) (err error) { + // TODO not implemented + panic("not implemented") +} diff --git a/radiance/genesis/testdata/mainnet/genesis.tar.bz2 b/radiance/genesis/testdata/mainnet/genesis.tar.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..a8ae5c67c9d851b9a28e390b88f4219ee9bf0224 GIT binary patch literal 34256 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 literal 0 HcmV?d00001 diff --git a/radiance/runtime/runtime.go b/radiance/runtime/runtime.go new file mode 100644 index 00000000..b944faa0 --- /dev/null +++ b/radiance/runtime/runtime.go @@ -0,0 +1,75 @@ +// Package runtime provides low-level components of the Solana Execution Layer. +package runtime + +import "time" + +type Account struct { + Lamports uint64 + Data []byte + Owner [32]byte + Executable bool + RentEpoch uint64 +} + +type PohParams struct { + TickDuration time.Duration + HasTickCount bool + TickCount uint64 + HasHashesPerTick bool + HashesPerTick uint64 +} + +type InflationParams struct { + Initial float64 + Terminal float64 + Taper float64 + Foundation float64 + FoundationTerm float64 + Padding00 [8]byte +} + +type EpochSchedule struct { + SlotPerEpoch uint64 + LeaderScheduleSlotOffset uint64 + Warmup bool + FirstNormalEpoch uint64 + FirstNormalSlot uint64 +} + +type FeeParams struct { + TargetLamportsPerSig uint64 + TargetSigsPerSlot uint64 + MinLamportsPerSig uint64 + MaxLamportsPerSig uint64 + BurnPercent uint8 +} + +type RentParams struct { + LamportsPerByteYear uint64 + ExemptionThreshold float64 + BurnPercent uint8 +} + +type Accounts interface { + GetAccount(pubkey *[32]byte) (*Account, error) + SetAccount(pubkey *[32]byte, acc *Account) error +} + +type MemAccounts struct { + Map map[[32]byte]*Account +} + +func NewMemAccounts() MemAccounts { + return MemAccounts{ + Map: make(map[[32]byte]*Account), + } +} + +func (m MemAccounts) GetAccount(pubkey *[32]byte) (*Account, error) { + return m.Map[*pubkey], nil +} + +func (m MemAccounts) SetAccount(pubkey *[32]byte, acc *Account) error { + m.Map[*pubkey] = acc + return nil +} diff --git a/radiance/runtime/serde.go b/radiance/runtime/serde.go new file mode 100644 index 00000000..855c3b83 --- /dev/null +++ b/radiance/runtime/serde.go @@ -0,0 +1,118 @@ +package runtime + +import ( + "fmt" + "io" + "math" + "time" + + bin "github.com/gagliardetto/binary" +) + +// Dumping ground for handwritten serialization boilerplate. +// To be removed when switching over to serde-generate. + +func (a *Account) UnmarshalWithDecoder(decoder *bin.Decoder) (err error) { + a.Lamports, err = decoder.ReadUint64(bin.LE) + if err != nil { + return err + } + var dataLen uint64 + dataLen, err = decoder.ReadUint64(bin.LE) + if err != nil { + return err + } + if dataLen > uint64(decoder.Remaining()) { + return io.ErrUnexpectedEOF + } + a.Data, err = decoder.ReadNBytes(int(dataLen)) + if err != nil { + return err + } + if err = decoder.Decode(&a.Owner); err != nil { + return err + } + a.Executable, err = decoder.ReadBool() + if err != nil { + return err + } + a.RentEpoch, err = decoder.ReadUint64(bin.LE) + return +} + +func (a *Account) MarshalWihEncoder(encoder *bin.Encoder) error { + _ = encoder.WriteUint64(a.Lamports, bin.LE) + _ = encoder.WriteUint64(uint64(len(a.Data)), bin.LE) + _ = encoder.WriteBytes(a.Data, false) + _ = encoder.WriteBytes(a.Owner[:], false) + _ = encoder.WriteBool(a.Executable) + return encoder.WriteUint64(a.RentEpoch, bin.LE) +} + +func (a *PohParams) UnmarshalWithDecoder(decoder *bin.Decoder) (err error) { + var tickDuration serdeDuration + if err = decoder.Decode(&tickDuration); err != nil { + return err + } + if a.TickDuration, err = tickDuration.Duration(); err != nil { + return err + } + if a.HasTickCount, err = decoder.ReadBool(); err != nil { + return err + } + if a.HasTickCount { + if a.TickCount, err = decoder.ReadUint64(bin.LE); err != nil { + return err + } + } + if a.HasHashesPerTick, err = decoder.ReadBool(); err != nil { + return err + } + if a.HasHashesPerTick { + if a.HashesPerTick, err = decoder.ReadUint64(bin.LE); err != nil { + return err + } + } + return nil +} + +func (a *PohParams) MarshalWithDecoder(encoder *bin.Encoder) (err error) { + tickDuration := newSerdeDuration(a.TickDuration) + _ = encoder.Encode(&tickDuration) + _ = encoder.WriteBool(a.HasTickCount) + if a.HasTickCount { + _ = encoder.WriteUint64(a.TickCount, bin.LE) + } + _ = encoder.WriteBool(a.HasHashesPerTick) + if a.HasHashesPerTick { + _ = encoder.WriteUint64(a.HashesPerTick, bin.LE) + } + return nil +} + +// serdeDuration implements the bincode serialization of std::time::Duration. +type serdeDuration struct { + Secs uint64 + Nanos uint32 +} + +func newSerdeDuration(d time.Duration) serdeDuration { + if d < 0 { + panic("negative duration") + } + return serdeDuration{ + Secs: uint64(d / time.Second), + Nanos: uint32(d % time.Second), + } +} + +func (s serdeDuration) Duration() (time.Duration, error) { + if time.Duration(s.Nanos) > time.Second { + return 0, fmt.Errorf("malformed serde duration") + } + if s.Secs > uint64(time.Duration(math.MaxInt64)/time.Second) { + return 0, fmt.Errorf("malformed serde duration") + } + d := time.Duration(s.Nanos) + (time.Duration(s.Secs) * time.Second) + return d, nil +} From 6cf9cafcbab312271b6af71149a451bb6a55d35a Mon Sep 17 00:00:00 2001 From: gagliardetto Date: Wed, 15 Nov 2023 16:17:02 +0100 Subject: [PATCH 6/9] Handle genesis file --- config.go | 18 +++++++++++++++++ epoch.go | 20 +++++++++++++++++++ genesis.go | 12 ++++++++++++ multiepoch-getBlock.go | 44 +++++++++++++++++++++++------------------- 4 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 genesis.go diff --git a/config.go b/config.go index a7699c4a..5451dcd9 100644 --- a/config.go +++ b/config.go @@ -125,6 +125,9 @@ type Config struct { URI URI `json:"uri" yaml:"uri"` } `json:"sig_exists" yaml:"sig_exists"` } `json:"indexes" yaml:"indexes"` + Genesis struct { + URI URI `json:"uri" yaml:"uri"` + } `json:"genesis" yaml:"genesis"` } func (c *Config) ConfigFilepath() string { @@ -296,5 +299,20 @@ func (c *Config) Validate() error { } } } + { + // if epoch is 0, then the genesis URI must be set: + if *c.Epoch == 0 { + if c.Genesis.URI.IsZero() { + return fmt.Errorf("epoch is 0, but genesis.uri is not set") + } + if !c.Genesis.URI.IsValid() { + return fmt.Errorf("genesis.uri is invalid") + } + // only support local genesis files for now: + if !c.Genesis.URI.IsLocal() { + return fmt.Errorf("genesis.uri must be a local file") + } + } + } return nil } diff --git a/epoch.go b/epoch.go index 73eed471..e921dbdd 100644 --- a/epoch.go +++ b/epoch.go @@ -22,6 +22,7 @@ import ( "github.com/rpcpool/yellowstone-faithful/gsfa" "github.com/rpcpool/yellowstone-faithful/ipld/ipldbindcode" "github.com/rpcpool/yellowstone-faithful/iplddecoders" + "github.com/rpcpool/yellowstone-faithful/radiance/genesis" "github.com/urfave/cli/v2" "k8s.io/klog/v2" ) @@ -30,6 +31,8 @@ type Epoch struct { epoch uint64 isFilecoinMode bool // true if the epoch is in Filecoin mode (i.e. Lassie mode) config *Config + // genesis: + genesis *GenesisContainer // contains indexes and block data for the epoch lassieFetcher *lassieWrapper localCarReader *carv2.Reader @@ -92,6 +95,10 @@ func (e *Epoch) Close() error { return errors.Join(multiErr...) } +func (e *Epoch) GetGenesis() *GenesisContainer { + return e.genesis +} + func NewEpochFromConfig(config *Config, c *cli.Context) (*Epoch, error) { if config == nil { return nil, fmt.Errorf("config must not be nil") @@ -105,6 +112,19 @@ func NewEpochFromConfig(config *Config, c *cli.Context) (*Epoch, error) { config: config, onClose: make([]func() error, 0), } + { + // if epoch is 0, then try loading the genesis from the config: + if *config.Epoch == 0 { + genesisConfig, ha, err := genesis.ReadGenesisFromFile(string(config.Genesis.URI)) + if err != nil { + return nil, fmt.Errorf("failed to read genesis: %w", err) + } + ep.genesis = &GenesisContainer{ + Hash: solana.HashFromBytes(ha[:]), + Config: genesisConfig, + } + } + } if isCarMode { // The CAR-mode requires a cid-to-offset index. diff --git a/genesis.go b/genesis.go new file mode 100644 index 00000000..ad91be58 --- /dev/null +++ b/genesis.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/gagliardetto/solana-go" + "github.com/rpcpool/yellowstone-faithful/radiance/genesis" +) + +type GenesisContainer struct { + Hash solana.Hash + // The genesis config. + Config *genesis.Genesis +} diff --git a/multiepoch-getBlock.go b/multiepoch-getBlock.go index 14686c59..16d28411 100644 --- a/multiepoch-getBlock.go +++ b/multiepoch-getBlock.go @@ -249,26 +249,6 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex } tim.time("get entries") - if slot == 0 { - // NOTE: we assume this is on mainnet. - blockZeroBlocktime := uint64(1584368940) - zeroBlockHeight := uint64(0) - blockZeroBlockHash := lastEntryHash.String() - var blockResp GetBlockResponse - blockResp.Transactions = make([]GetTransactionResponse, 0) - blockResp.BlockTime = &blockZeroBlocktime - blockResp.Blockhash = lastEntryHash.String() - blockResp.ParentSlot = uint64(0) - blockResp.Rewards = make([]any, 0) - blockResp.BlockHeight = &zeroBlockHeight - blockResp.PreviousBlockhash = &blockZeroBlockHash // NOTE: this is what solana RPC does. Should it be nil instead? Or should it be the genesis hash? - return nil, conn.ReplyRaw( - ctx, - req.ID, - blockResp, - ) - } - var allTransactions []GetTransactionResponse var rewards any hasRewards := !block.Rewards.(cidlink.Link).Cid.Equals(DummyCID) @@ -423,6 +403,21 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex blockResp.ParentSlot = uint64(block.Meta.Parent_slot) blockResp.Rewards = rewards + if slot == 0 { + genesis := epochHandler.GetGenesis() + if genesis != nil { + blockZeroBlocktime := uint64(genesis.Config.CreationTime.Unix()) + blockResp.BlockTime = &blockZeroBlocktime + } + blockResp.ParentSlot = uint64(0) + + zeroBlockHeight := uint64(0) + blockResp.BlockHeight = &zeroBlockHeight + + blockZeroBlockHash := lastEntryHash.String() + blockResp.PreviousBlockhash = &blockZeroBlockHash // NOTE: this is what solana RPC does. Should it be nil instead? Or should it be the genesis hash? + } + { blockHeight, ok := block.GetBlockHeight() if ok { @@ -461,6 +456,15 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex } tim.time("get parent block") + { + if len(blockResp.Transactions) == 0 { + blockResp.Transactions = make([]GetTransactionResponse, 0) + } + if blockResp.Rewards == nil || len(blockResp.Rewards.([]any)) == 0 { + blockResp.Rewards = make([]any, 0) + } + } + err = conn.Reply( ctx, req.ID, From 929a512b442d9667392bb462c7ccf5912564d7d4 Mon Sep 17 00:00:00 2001 From: gagliardetto Date: Wed, 15 Nov 2023 16:24:00 +0100 Subject: [PATCH 7/9] Handle getGenesisHash --- multiepoch-getGenesisHash.go | 41 ++++++++++++++++++++++++++++++++++++ multiepoch.go | 4 +++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 multiepoch-getGenesisHash.go diff --git a/multiepoch-getGenesisHash.go b/multiepoch-getGenesisHash.go new file mode 100644 index 00000000..381c519d --- /dev/null +++ b/multiepoch-getGenesisHash.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "fmt" + + "github.com/sourcegraph/jsonrpc2" +) + +func (multi *MultiEpoch) handleGetGenesisHash(ctx context.Context, conn *requestContext, req *jsonrpc2.Request) (*jsonrpc2.Error, error) { + // Epoch 0 contains the genesis config. + epochNumber := uint64(0) + epochHandler, err := multi.GetEpoch(epochNumber) + if err != nil { + // If epoch 0 is not available, then the genesis config is not available. + return &jsonrpc2.Error{ + Code: CodeNotFound, + Message: fmt.Sprintf("Epoch %d is not available", epochNumber), + }, fmt.Errorf("failed to get epoch %d: %w", epochNumber, err) + } + + genesis := epochHandler.GetGenesis() + if genesis == nil { + return &jsonrpc2.Error{ + Code: CodeNotFound, + Message: "Genesis is not available", + }, fmt.Errorf("genesis is nil") + } + + genesisHash := genesis.Hash + + err = conn.ReplyRaw( + ctx, + req.ID, + genesisHash.String(), + ) + if err != nil { + return nil, fmt.Errorf("failed to reply: %w", err) + } + return nil, nil +} diff --git a/multiepoch.go b/multiepoch.go index 79628ae4..c5777dfe 100644 --- a/multiepoch.go +++ b/multiepoch.go @@ -401,7 +401,7 @@ func sanitizeMethod(method string) string { func isValidLocalMethod(method string) bool { switch method { - case "getBlock", "getTransaction", "getSignaturesForAddress", "getBlockTime": + case "getBlock", "getTransaction", "getSignaturesForAddress", "getBlockTime", "getGenesisHash": return true default: return false @@ -419,6 +419,8 @@ func (ser *MultiEpoch) handleRequest(ctx context.Context, conn *requestContext, return ser.handleGetSignaturesForAddress(ctx, conn, req) case "getBlockTime": return ser.handleGetBlockTime(ctx, conn, req) + case "getGenesisHash": + return ser.handleGetGenesisHash(ctx, conn, req) default: return &jsonrpc2.Error{ Code: jsonrpc2.CodeMethodNotFound, From 4d41ba312c43a131c170c64b4d088ee666642f08 Mon Sep 17 00:00:00 2001 From: gagliardetto Date: Thu, 16 Nov 2023 15:13:53 +0100 Subject: [PATCH 8/9] Fix prefetching --- cmd-rpc.go | 6 +++++ epoch.go | 49 +++++++++++++++++++++++++----------- multiepoch-getBlock.go | 56 ++++++++++++++++++++---------------------- multiepoch.go | 10 ++++++++ 4 files changed, 77 insertions(+), 44 deletions(-) diff --git a/cmd-rpc.go b/cmd-rpc.go index 6e46bb54..15d3ecca 100644 --- a/cmd-rpc.go +++ b/cmd-rpc.go @@ -153,6 +153,12 @@ func newCmd_rpc() *cli.Command { EpochSearchConcurrency: epochSearchConcurrency, }) + defer func() { + if err := multi.Close(); err != nil { + klog.Errorf("error closing multi-epoch: %s", err.Error()) + } + }() + for _, epoch := range epochs { if err := multi.AddEpoch(epoch.Epoch(), epoch); err != nil { return cli.Exit(fmt.Sprintf("failed to add epoch %d: %s", epoch.Epoch(), err.Error()), 1) diff --git a/epoch.go b/epoch.go index e921dbdd..f83165e1 100644 --- a/epoch.go +++ b/epoch.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "bytes" "context" "crypto/rand" "encoding/binary" @@ -12,6 +13,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/ipfs/go-cid" + carv1 "github.com/ipld/go-car" "github.com/ipld/go-car/util" carv2 "github.com/ipld/go-car/v2" "github.com/libp2p/go-libp2p/core/peer" @@ -34,19 +36,19 @@ type Epoch struct { // genesis: genesis *GenesisContainer // contains indexes and block data for the epoch - lassieFetcher *lassieWrapper - localCarReader *carv2.Reader - remoteCarReader ReaderAtCloser - remoteCarHeaderSize uint64 - cidToOffsetIndex *compactindex.DB - slotToCidIndex *compactindex36.DB - sigToCidIndex *compactindex36.DB - sigExists *bucketteer.Reader - gsfaReader *gsfa.GsfaReader - cidToNodeCache *cache.Cache // TODO: prevent OOM - onClose []func() error - slotToCidCache *cache.Cache - cidToOffsetCache *cache.Cache + lassieFetcher *lassieWrapper + localCarReader *carv2.Reader + remoteCarReader ReaderAtCloser + carHeaderSize uint64 + cidToOffsetIndex *compactindex.DB + slotToCidIndex *compactindex36.DB + sigToCidIndex *compactindex36.DB + sigExists *bucketteer.Reader + gsfaReader *gsfa.GsfaReader + cidToNodeCache *cache.Cache // TODO: prevent OOM + onClose []func() error + slotToCidCache *cache.Cache + cidToOffsetCache *cache.Cache } func (r *Epoch) getSlotToCidFromCache(slot uint64) (cid.Cid, error, bool) { @@ -227,7 +229,7 @@ func NewEpochFromConfig(config *Config, c *cli.Context) (*Epoch, error) { ep.localCarReader = localCarReader ep.remoteCarReader = remoteCarReader if remoteCarReader != nil { - // read 10 bytes from the CAR file to get the header size + // determine the header size so that we know where the data starts: headerSizeBuf, err := readSectionFromReaderAt(remoteCarReader, 0, 10) if err != nil { return nil, fmt.Errorf("failed to read CAR header: %w", err) @@ -237,7 +239,24 @@ func NewEpochFromConfig(config *Config, c *cli.Context) (*Epoch, error) { if n <= 0 { return nil, fmt.Errorf("failed to decode CAR header size") } - ep.remoteCarHeaderSize = uint64(n) + headerSize + ep.carHeaderSize = uint64(n) + headerSize + } + if localCarReader != nil { + // determine the header size so that we know where the data starts: + dr, err := localCarReader.DataReader() + if err != nil { + return nil, fmt.Errorf("failed to get local CAR data reader: %w", err) + } + header, err := readHeader(dr) + if err != nil { + return nil, fmt.Errorf("failed to read local CAR header: %w", err) + } + var buf bytes.Buffer + if err = carv1.WriteHeader(header, &buf); err != nil { + return nil, fmt.Errorf("failed to encode local CAR header: %w", err) + } + headerSize := uint64(buf.Len()) + ep.carHeaderSize = headerSize } } { diff --git a/multiepoch-getBlock.go b/multiepoch-getBlock.go index 16d28411..ddcef1f1 100644 --- a/multiepoch-getBlock.go +++ b/multiepoch-getBlock.go @@ -69,12 +69,15 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex tim.time("GetBlock") { prefetcherFromCar := func() error { + parentIsInPreviousEpoch := CalcEpochForSlot(uint64(block.Meta.Parent_slot)) != CalcEpochForSlot(slot) if slot == 0 { - return nil + parentIsInPreviousEpoch = true + } + if slot > 1 && block.Meta.Parent_slot == 0 { + parentIsInPreviousEpoch = true } - parentIsInPreviousEpoch := CalcEpochForSlot(uint64(block.Meta.Parent_slot)) != CalcEpochForSlot(slot) - var blockCid, parentCid cid.Cid + var blockCid, parentBlockCid cid.Cid wg := new(errgroup.Group) wg.Go(func() (err error) { blockCid, err = epochHandler.FindCidFromSlot(ctx, slot) @@ -87,7 +90,7 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex if parentIsInPreviousEpoch { return nil } - parentCid, err = epochHandler.FindCidFromSlot(ctx, uint64(block.Meta.Parent_slot)) + parentBlockCid, err = epochHandler.FindCidFromSlot(ctx, uint64(block.Meta.Parent_slot)) if err != nil { return err } @@ -97,7 +100,17 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex if err != nil { return err } - klog.Infof("%s -> %s", parentCid, blockCid) + if slot == 0 { + klog.Infof("car start to slot(0)::%s", blockCid) + } else { + klog.Infof( + "slot(%d)::%s to slot(%d)::%s", + uint64(block.Meta.Parent_slot), + parentBlockCid, + slot, + blockCid, + ) + } { var blockOffset, parentOffset uint64 wg := new(errgroup.Group) @@ -111,13 +124,12 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex wg.Go(func() (err error) { if parentIsInPreviousEpoch { // get car file header size - parentOffset = epochHandler.remoteCarHeaderSize + parentOffset = epochHandler.carHeaderSize return nil } - parentOffset, err = epochHandler.FindOffsetFromCid(ctx, parentCid) + parentOffset, err = epochHandler.FindOffsetFromCid(ctx, parentBlockCid) if err != nil { - // If the parent is not found, it (probably) means that it's outside of the car file. - parentOffset = epochHandler.remoteCarHeaderSize + return err } return nil }) @@ -128,23 +140,12 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex length := blockOffset - parentOffset MiB := uint64(1024 * 1024) - maxSize := MiB * 100 - if length > maxSize { - length = maxSize + maxPrefetchSize := MiB * 10 // let's cap prefetching size + if length > maxPrefetchSize { + length = maxPrefetchSize } - idealEntrySize := uint64(36190) - var start uint64 - if parentIsInPreviousEpoch { - start = parentOffset - } else { - if parentOffset > idealEntrySize { - start = parentOffset - idealEntrySize - } else { - start = parentOffset - } - length += idealEntrySize - } + start := parentOffset klog.Infof("prefetching CAR: start=%d length=%d (parent_offset=%d)", start, length, parentOffset) carSection, err := epochHandler.ReadAtFromCar(ctx, start, length) @@ -152,17 +153,14 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex return err } dr := bytes.NewReader(carSection) - if !parentIsInPreviousEpoch { - dr.Seek(int64(idealEntrySize), io.SeekStart) - } br := bufio.NewReader(dr) gotCid, data, err := util.ReadNode(br) if err != nil { return fmt.Errorf("failed to read first node: %w", err) } - if !parentIsInPreviousEpoch && !gotCid.Equals(parentCid) { - return fmt.Errorf("CID mismatch: expected %s, got %s", parentCid, gotCid) + if !parentIsInPreviousEpoch && !gotCid.Equals(parentBlockCid) { + return fmt.Errorf("CID mismatch: expected %s, got %s", parentBlockCid, gotCid) } epochHandler.putNodeInCache(gotCid, data) diff --git a/multiepoch.go b/multiepoch.go index c5777dfe..b2763c29 100644 --- a/multiepoch.go +++ b/multiepoch.go @@ -155,6 +155,16 @@ func (m *MultiEpoch) GetFirstAvailableEpochNumber() (uint64, error) { return 0, fmt.Errorf("no epochs available") } +func (m *MultiEpoch) Close() error { + m.mu.Lock() + defer m.mu.Unlock() + klog.Info("Closing all epochs...") + for _, ep := range m.epochs { + ep.Close() + } + return nil +} + type ListenerConfig struct { ProxyConfig *ProxyConfig } From 26e5dad393c0bb561afae83a448c11d50bdd31a8 Mon Sep 17 00:00:00 2001 From: gagliardetto Date: Fri, 17 Nov 2023 15:28:34 +0100 Subject: [PATCH 9/9] Cleanup logs --- multiepoch-getBlock.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/multiepoch-getBlock.go b/multiepoch-getBlock.go index ddcef1f1..9d397b98 100644 --- a/multiepoch-getBlock.go +++ b/multiepoch-getBlock.go @@ -226,8 +226,6 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex klog.Errorf("failed to decode Transaction %s: %v", tcid, err) return nil } - // NOTE: this messes up the order of transactions, - // but we sort them later anyway. mu.Lock() allTransactionNodes[entryIndex][txI] = txNode mu.Unlock() @@ -449,7 +447,9 @@ func (multi *MultiEpoch) handleGetBlock(ctx context.Context, conn *requestContex blockResp.PreviousBlockhash = &parentEntryHash } } else { - klog.Infof("parent slot is in a different epoch, not implemented yet (can't get previousBlockhash)") + if slot != 0 { + klog.Infof("parent slot is in a different epoch, not implemented yet (can't get previousBlockhash)") + } } } tim.time("get parent block")