Skip to content

Add payer reports table #703

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: 04-04-add_support_for_new_envelope_types
Choose a base branch
from

Conversation

neekolas
Copy link
Contributor

@neekolas neekolas commented Apr 4, 2025

Add database schema and SQL queries to store payer reports and their attestations

  • Creates new payer_reports table to store report metadata including merkle roots, sequence IDs, and submission status in 00011_store-payer-reports.up.sql
  • Creates new payer_report_attestations table to store node signatures with foreign key relationships in 00011_store-payer-reports.up.sql
  • Adds SQL queries InsertOrIgnorePayerReport and InsertPayerReportAttestation in payerReports.sql for handling report and attestation insertions with duplicate prevention

📍Where to Start

Start with the schema definitions in 00011_store-payer-reports.up.sql to understand the data structure, then review the SQL queries in payerReports.sql that interact with these tables.


Macroscope summarized 39ab894.

Summary by CodeRabbit

  • New Features
    • Introduced structured support for storing, retrieving, and managing payer reports and attestations with conflict handling.
    • Added detailed payer report data structures and enhanced report generation with randomized Merkle roots for improved integrity.
  • Chores
    • Improved SQL query formatting and database schema with indexing for better performance.
    • Enhanced data validation and error handling for payer report storage operations.

Copy link
Contributor

coderabbitai bot commented Apr 4, 2025

Walkthrough

This pull request refactors existing SQL queries in payerReports.sql for improved formatting and adds new SQL commands to insert payer reports and attestations with conflict handling. It introduces new migration files to create and drop the payer_reports and payer_report_attestations tables with appropriate columns, constraints, and indexes. The data model is extended with new Go structs representing payer reports and attestations. The payer report handling logic is restructured by removing the old PayerReport struct from the interface, splitting the report manager interface, and introducing a new FullPayerReport type. A new payerreport package is added, defining report data structures, ID computation, and protobuf conversion. A new Store type is implemented to persist and retrieve reports and attestations with validation and error handling. Corresponding unit tests are added. Additionally, utility function SliceToArray32 is updated to include input validation.

Changes

File(s) Summary of Changes
pkg/db/sqlc/payerReports.sql Reformatted existing queries for readability; added new SQL commands: InsertOrIgnorePayerReport, InsertOrIgnorePayerReportAttestation, and FetchPayerReport to support insertion and retrieval of payer reports and attestations with conflict handling.
pkg/migrations/00011_store-payer-reports.up.sql
pkg/migrations/00011_store-payer-reports.down.sql
Added migration files: UP migration creates payer_reports and payer_report_attestations tables with columns, constraints, and indexes; DOWN migration drops these tables if they exist.
pkg/db/queries/models.go Added new struct types PayerReport and PayerReportAttestation representing the new tables.
pkg/db/queries/payerReports.sql.go Added Go methods for new SQL commands: FetchPayerReport, InsertOrIgnorePayerReport, and InsertOrIgnorePayerReportAttestation; reformatted existing queries for consistency.
pkg/payerreport/interface.go Removed old PayerReport struct and its methods; split IPayerReportManager interface into PayerReportGenerator and PayerReportAttester; added composite interface IPayerReportManager and new interface IPayerReportStore for storage operations.
pkg/payerreport/report.go Added new PayerReport and FullPayerReport structs with methods for protobuf conversion and ID computation using ABI packing and hashing.
pkg/payerreport/manager.go Modified GenerateReport method to return *FullPayerReport instead of *PayerReport; added helper function randomBytes32 for generating random 32-byte arrays; updated logger initialization.
pkg/payerreport/store.go Added Store type with methods to store and fetch payer reports and attestations, including validation for node ID and count ranges, and error handling.
pkg/payerreport/store_test.go Added unit tests for the Store methods verifying storing, retrieving, error conditions, and idempotency.
pkg/payerreport/report_test.go Updated test to use randomized 32-byte arrays for Merkle roots and hashes instead of fixed short byte slices.
pkg/payerreport/id.go Renamed two ABI argument fields: "nodesMerkleRoot" to "nodesHash" and "nodesLeafCount" to "nodesCount" without changing types or sizes.
pkg/utils/slice.go Updated SliceToArray32 function to include input length validation and return an error if the input slice is not exactly 32 bytes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PayerReportStore
    participant Database

    Client->>PayerReportStore: StoreReport(report)
    PayerReportStore->>Database: InsertOrIgnorePayerReport SQL query
    Database-->>PayerReportStore: Insert result (or conflict resolution)
    PayerReportStore-->>Client: ReportID or error

    Client->>PayerReportStore: StoreAttestation(attestation)
    PayerReportStore->>Database: InsertOrIgnorePayerReportAttestation SQL query
    Database-->>PayerReportStore: Insert result (or conflict resolution)
    PayerReportStore-->>Client: success or error

    Client->>PayerReportStore: FetchReport(reportID)
    PayerReportStore->>Database: FetchPayerReport SQL query
    Database-->>PayerReportStore: PayerReport data
    PayerReportStore-->>Client: PayerReport or error
Loading

Suggested reviewers

  • mkysel

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (1.64.8)

Error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2
Failed executing command with error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

This was referenced Apr 4, 2025
Copy link
Contributor Author

neekolas commented Apr 4, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
pkg/migrations/00011_store-payer-reports.up.sql (2)

1-12: Validate 'payer_reports' table schema design.
The table schema is clear and meets the reporting requirements. As a nitpick, verify that the use of the BYTEA data type for the primary key (id) is intentional and consider whether created_at should be timezone-aware using TIMESTAMPTZ for consistency.


24-24: Consider assigning explicit names to indexes.
While the index on payer_report_attestations (payer_report_id) is correctly set up, explicitly naming indexes (e.g., using CREATE INDEX idx_payer_report_attestations_payer_report_id ON payer_report_attestations(payer_report_id);) can improve maintainability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7d1bea7 and 39ab894.

📒 Files selected for processing (3)
  • pkg/db/sqlc/payerReports.sql (1 hunks)
  • pkg/migrations/00011_store-payer-reports.down.sql (1 hunks)
  • pkg/migrations/00011_store-payer-reports.up.sql (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Push Docker Images to GitHub Packages (xmtpd-cli)
  • GitHub Check: Push Docker Images to GitHub Packages (xmtpd)
  • GitHub Check: Build pre-baked anvil-xmtpd
  • GitHub Check: Test (Node)
  • GitHub Check: Upgrade Tests
🔇 Additional comments (10)
pkg/migrations/00011_store-payer-reports.up.sql (2)

14-14: Index creation on submission_status.
The index on submission_status is appropriate for optimizing queries filtering by report status.


16-22: Define 'payer_report_attestations' table with proper constraints.
The table structure is well-defined with a composite primary key and a foreign key referencing payer_reports. The use of ON DELETE CASCADE ensures referential integrity upon deletion.

pkg/db/sqlc/payerReports.sql (8)

1-7: 'FindOrCreatePayer' upsert statement review.
The SQL upsert pattern is clearly formatted. Just ensure that a unique constraint or index on address exists in the payers table to support the ON CONFLICT clause effectively.


8-26: Incrementing usage with upsert logic.
The IncrementUnsettledUsage query uses an effective upsert pattern with proper aggregation using GREATEST. The formatting improvements enhance readability.


27-40: Aggregation query in 'GetPayerUnsettledUsage'.
The query effectively uses COALESCE to handle nulls and provides a clear aggregation of values.


41-50: 'BuildPayerReport' reporting query review.
The join between unsettled_usage and payers along with the grouping is clear and appropriately structured for generating the report.


51-56: Review of 'GetGatewayEnvelopeByID'.
The query is concise with a consolidated WHERE clause. The inline comment about leveraging the primary key index is helpful.


57-72: Effective use of CTE in 'GetSecondNewestMinute'.
The common table expression (CTE) neatly encapsulates querying the second newest minute. The use of LIMIT and OFFSET appears appropriate for this purpose.


73-92: Insertion command 'InsertOrIgnorePayerReport'.
The statement properly handles insertions into payer_reports by using ON CONFLICT DO NOTHING to avoid duplicate entries. All required columns are supplied while relying on table defaults (e.g., for created_at).


93-95: Insertion command 'InsertPayerReportAttestation'.
The command effectively avoids inserting duplicate attestations with the composite key (payer_report_id, node_id) by using ON CONFLICT DO NOTHING.

Comment on lines +1 to +3
DROP TABLE IF EXISTS payer_reports;

DROP TABLE IF EXISTS payer_report_attestations;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Reorder DROP statements to respect foreign key dependencies.
Since payer_report_attestations references payer_reports, dropping the parent table first might cause a dependency error. It is recommended to drop the child table first.

Proposed diff:

- DROP TABLE IF EXISTS payer_reports;
- 
- DROP TABLE IF EXISTS payer_report_attestations;
+ DROP TABLE IF EXISTS payer_report_attestations;
+ 
+ DROP TABLE IF EXISTS payer_reports;

Action Required: Reorder DROP Statements in Migration File

The current migration file contains the following statements:

DROP TABLE IF EXISTS payer_reports;

DROP TABLE IF EXISTS payer_report_attestations;

Since the table payer_report_attestations references payer_reports, dropping the parent table (payer_reports) first can trigger a dependency error. Please update the file by dropping the child table first. The corrected order should be:

- DROP TABLE IF EXISTS payer_reports;
- 
- DROP TABLE IF EXISTS payer_report_attestations;
+ DROP TABLE IF EXISTS payer_report_attestations;
+ 
+ DROP TABLE IF EXISTS payer_reports;

@neekolas neekolas force-pushed the 04-04-add_support_for_new_envelope_types branch from 7d1bea7 to 79ffefb Compare April 18, 2025 21:47
@neekolas neekolas force-pushed the 04-04-add_payer_reports_table branch from 39ab894 to 49c4502 Compare April 18, 2025 21:47
EndSequenceId: p.EndSequenceID,
PayersMerkleRoot: p.PayersMerkleRoot[:],
PayersLeafCount: p.PayersLeafCount,
}
Copy link

Choose a reason for hiding this comment

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

In the ToProto function, the NodesHash and NodesCount fields of the PayerReport struct are not being mapped to the corresponding NodesMerkleRoot and NodesLeafCount fields in the proto message. If these node details are intended to be included in the proto representation, consider mapping them accordingly. If this was an intentional omission, please consider documenting why these fields are omitted.

+		NodesMerkleRoot:  p.NodesHash[:],
+		NodesLeafCount:   p.NodesCount,

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
pkg/payerreport/manager.go (1)

51-63: Indicate when using placeholder data.

The function returns a random merkle root as a placeholder, but doesn't clearly indicate this in the returned structure. Consider adding a flag or field to indicate when using non-production ready placeholder values.

return &FullPayerReport{
  PayerReport: PayerReport{
    OriginatorNodeID: uint32(originatorID),
    StartSequenceID:  params.LastReportEndSequenceID,
    EndSequenceID:    params.LastReportEndSequenceID,
    // TODO: Implement merkle calculation
    PayersMerkleRoot: randomBytes32(),
    PayersLeafCount:  uint32(0),
+   IsPlaceholder:    true, // Indicate this contains placeholder data
  },
  Payers: make(map[common.Address]currency.PicoDollar),
}, nil
pkg/payerreport/report.go (1)

29-34: Add method to convert from protobuf to Go struct.

For complete serialization support, consider adding a static method to convert from protobuf message back to FullPayerReport or PayerReport struct.

// FromProto creates a PayerReport from its protobuf representation
func PayerReportFromProto(proto *proto.PayerReport) (*PayerReport, error) {
  if proto == nil {
    return nil, fmt.Errorf("nil protobuf message")
  }
  
  var payersMerkleRoot, nodesHash [32]byte
  
  // Handle potential size mismatch
  if len(proto.PayersMerkleRoot) != 32 {
    return nil, fmt.Errorf("invalid payers merkle root size: %d", len(proto.PayersMerkleRoot))
  }
  copy(payersMerkleRoot[:], proto.PayersMerkleRoot)
  
  // If nodesHash is present in proto
  if proto.NodesHash != nil {
    if len(proto.NodesHash) != 32 {
      return nil, fmt.Errorf("invalid nodes hash size: %d", len(proto.NodesHash))
    }
    copy(nodesHash[:], proto.NodesHash)
  }
  
  return &PayerReport{
    OriginatorNodeID: proto.OriginatorNodeId,
    StartSequenceID:  proto.StartSequenceId,
    EndSequenceID:    proto.EndSequenceId,
    PayersMerkleRoot: payersMerkleRoot,
    PayersLeafCount:  proto.PayersLeafCount,
    NodesHash:        nodesHash,
    NodesCount:       proto.NodesCount,
  }, nil
}
pkg/payerreport/interface.go (1)

31-36: Consider updating AttestReport to use FullPayerReport.

The AttestReport method parameters use *PayerReport while GenerateReport returns *FullPayerReport. For consistency, consider updating the interface to use the same types, or document why different types are used.

type PayerReportAttester interface {
  AttestReport(
    ctx context.Context,
-   prevReport *PayerReport,
-   newReport *PayerReport,
+   prevReport *FullPayerReport,
+   newReport *FullPayerReport,
  ) (*PayerReportAttestation, error)
}
pkg/migrations/00011_store-payer-reports.up.sql (1)

1-13: Consider using an enum type for submission_status.

The submission_status field uses numeric values (0, 1, 2) with a comment explaining their meaning. Using a PostgreSQL enum type would make this more self-documenting and prevent invalid states.

+CREATE TYPE payer_report_status AS ENUM ('pending', 'submitted', 'settled');
+
 CREATE TABLE payer_reports (
     id BYTEA PRIMARY KEY,
     originator_node_id INT NOT NULL,
     start_sequence_id BIGINT NOT NULL,
     end_sequence_id BIGINT NOT NULL,
     payers_merkle_root BYTEA NOT NULL,
     payers_leaf_count BIGINT NOT NULL,
     nodes_hash BYTEA NOT NULL,
     nodes_count INT NOT NULL,
-    -- 0 = pending, 1 = submitted, 2 = settled
-    submission_status SMALLINT NOT NULL DEFAULT 0,
+    submission_status payer_report_status NOT NULL DEFAULT 'pending',
     created_at TIMESTAMP DEFAULT NOW()
 );
pkg/payerreport/store.go (2)

37-45: Consider using constants for clarity

While the current validation logic is correct, using named constants for the maximum values would improve code readability.

-	// The originator node ID is stored as an int32 in the database, but
-	// a uint32 on the network. Do not allow anything larger than max int32
-	if report.OriginatorNodeID > math.MaxInt32 {
-		return nil, ErrOriginatorNodeIDTooLarge
-	}
-
-	if report.NodesCount > math.MaxInt32 {
-		return nil, ErrNodesCountTooLarge
-	}
+	// The originator node ID and nodes count are stored as int32 in the database, but
+	// as uint32 in the domain model. Validate they don't exceed max int32
+	const maxDBInt32 = math.MaxInt32
+	
+	if report.OriginatorNodeID > maxDBInt32 {
+		return nil, ErrOriginatorNodeIDTooLarge
+	}
+
+	if report.NodesCount > maxDBInt32 {
+		return nil, ErrNodesCountTooLarge
+	}

85-92: Consider consolidating error handling for array conversions

There's some repetition in the error handling for SliceToArray32 conversions. Consider using a helper function to reduce duplication.

-	var payersMerkleRoot [32]byte
-	var nodesHash [32]byte
-	if payersMerkleRoot, err = utils.SliceToArray32(report.PayersMerkleRoot); err != nil {
-		return nil, err
-	}
-	if nodesHash, err = utils.SliceToArray32(report.NodesHash); err != nil {
-		return nil, err
-	}
+	// Helper function to convert slice to array with error handling
+	convertToArray32 := func(slice []byte) ([32]byte, error) {
+		return utils.SliceToArray32(slice)
+	}
+	
+	payersMerkleRoot, err := convertToArray32(report.PayersMerkleRoot)
+	if err != nil {
+		return nil, err
+	}
+	
+	nodesHash, err := convertToArray32(report.NodesHash)
+	if err != nil {
+		return nil, err
+	}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39ab894 and 49c4502.

⛔ Files ignored due to path filters (1)
  • dev/gen/sqlc is excluded by !**/gen/**
📒 Files selected for processing (13)
  • pkg/db/queries/models.go (1 hunks)
  • pkg/db/queries/payerReports.sql.go (7 hunks)
  • pkg/db/sqlc/payerReports.sql (1 hunks)
  • pkg/migrations/00011_store-payer-reports.down.sql (1 hunks)
  • pkg/migrations/00011_store-payer-reports.up.sql (1 hunks)
  • pkg/payerreport/id.go (1 hunks)
  • pkg/payerreport/interface.go (1 hunks)
  • pkg/payerreport/manager.go (5 hunks)
  • pkg/payerreport/report.go (1 hunks)
  • pkg/payerreport/report_test.go (1 hunks)
  • pkg/payerreport/store.go (1 hunks)
  • pkg/payerreport/store_test.go (1 hunks)
  • pkg/utils/slice.go (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • pkg/payerreport/id.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/migrations/00011_store-payer-reports.down.sql
  • pkg/db/sqlc/payerReports.sql
🧰 Additional context used
🧬 Code Graph Analysis (5)
pkg/payerreport/manager.go (4)
pkg/payerreport/interface.go (1)
  • PayerReportGenerationParams (17-21)
pkg/payerreport/report.go (2)
  • FullPayerReport (29-34)
  • PayerReport (12-27)
pkg/db/queries/models.go (1)
  • PayerReport (64-75)
pkg/currency/currency.go (1)
  • PicoDollar (10-10)
pkg/payerreport/store.go (6)
pkg/db/queries/db.go (2)
  • New (19-21)
  • Queries (23-25)
pkg/db/queries/models.go (2)
  • PayerReport (64-75)
  • PayerReportAttestation (77-82)
pkg/payerreport/report.go (2)
  • PayerReport (12-27)
  • ReportID (10-10)
pkg/db/queries/payerReports.sql.go (2)
  • InsertOrIgnorePayerReportParams (251-260)
  • InsertOrIgnorePayerReportAttestationParams (281-285)
pkg/payerreport/interface.go (2)
  • PayerReportAttestation (12-15)
  • NodeSignature (7-10)
pkg/utils/slice.go (1)
  • SliceToArray32 (9-17)
pkg/payerreport/report.go (3)
pkg/db/queries/models.go (1)
  • PayerReport (64-75)
pkg/currency/currency.go (1)
  • PicoDollar (10-10)
pkg/utils/hash.go (1)
  • HashPayerReportInput (35-37)
pkg/payerreport/interface.go (2)
pkg/payerreport/report.go (3)
  • FullPayerReport (29-34)
  • PayerReport (12-27)
  • ReportID (10-10)
pkg/db/queries/models.go (2)
  • PayerReport (64-75)
  • PayerReportAttestation (77-82)
pkg/db/queries/payerReports.sql.go (3)
pkg/db/queries/db.go (1)
  • Queries (23-25)
pkg/db/queries/models.go (1)
  • PayerReport (64-75)
pkg/payerreport/report.go (1)
  • PayerReport (12-27)
🪛 GitHub Check: Lint-Go
pkg/utils/slice.go

[failure] 5-5:
File is not properly formatted (gofumpt)

🪛 GitHub Actions: Lint
pkg/utils/slice.go

[error] 5-5: File is not properly formatted (gofumpt)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Push Docker Images to GitHub Packages (xmtpd-cli)
  • GitHub Check: Push Docker Images to GitHub Packages (xmtpd-cli)
  • GitHub Check: Push Docker Images to GitHub Packages (xmtpd)
  • GitHub Check: Upgrade Tests
  • GitHub Check: Test (Node)
  • GitHub Check: Upgrade Tests
  • GitHub Check: Push Docker Images to GitHub Packages (xmtpd)
  • GitHub Check: Test (Node)
  • GitHub Check: Code Review
🔇 Additional comments (25)
pkg/utils/slice.go (1)

9-17: LGTM: Good enhancement with input validation

The updated function now properly validates that the input slice is exactly 32 bytes long before proceeding with the copy operation. This is a good improvement for error handling and ensures safety when dealing with fixed-size byte arrays.

pkg/payerreport/report_test.go (1)

14-16: LGTM: Updated field names and test data generation

The test has been updated to use the randomBytes32() helper function for generating test data instead of fixed byte slices, and uses the correct field name NodesHash which aligns with the updated struct definition.

pkg/db/queries/models.go (2)

64-75: LGTM: Well-structured PayerReport model

The new PayerReport struct correctly defines all the fields needed to represent a payer report in the database, with appropriate types for each field, including the properly sized ID, merkle roots, and timestamps.


77-82: LGTM: Well-structured PayerReportAttestation model

The new PayerReportAttestation struct correctly models the relationship between attestations and payer reports using the PayerReportID field as a foreign key reference, along with appropriate fields for node identification, signatures, and timestamps.

pkg/payerreport/store_test.go (3)

13-19: LGTM: Good test setup helper

The createTestStore helper function properly sets up a test environment with a new database and logger, and ensures cleanup after the test completes.


21-84: LGTM: Comprehensive test cases for storing and retrieving reports

This test function covers both valid and invalid scenarios:

  • Testing a valid report with proper field values
  • Testing validation failures for invalid node IDs that exceed int32 range
  • Testing validation failures for invalid node counts that exceed int32 range

The test properly verifies that stored reports can be fetched and that they match the original data.


86-110: LGTM: Effective idempotency test

This test effectively verifies that storing the same report multiple times returns the same report ID, confirming the idempotent behavior of the store operation. The test correctly:

  1. Generates a report ID directly from the report
  2. Stores the report and verifies the returned ID matches
  3. Stores the same report again and verifies the returned ID is the same

This ensures that duplicate reports won't be stored multiple times in the database.

pkg/payerreport/manager.go (2)

57-59: Create tracking issue for TODO item.

There's a TODO comment for implementing merkle calculation without a linked ticket or issue number, which risks it being forgotten.

Consider adding a specific issue number to track this implementation work:

- // TODO: Implement merkle calculation
+ // TODO(#xxx): Implement merkle calculation

25-25: Change to named logger is a good practice.

Using a named logger helps with log filtering and organization, making it easier to identify the source of log messages.

pkg/payerreport/interface.go (2)

23-28: Good interface refactoring.

Breaking out the PayerReportGenerator interface from the broader IPayerReportManager improves modularity and separation of concerns. This makes the code more maintainable and testable.


43-47: Well-designed storage interface.

The IPayerReportStore interface provides a clean abstraction for storage operations with clear method signatures. The separation of storage concerns from business logic is a good architectural practice.

pkg/migrations/00011_store-payer-reports.up.sql (1)

26-26: Good index creation for frequent queries.

Creating an index on payer_report_id in the attestations table is a good practice for optimizing queries that filter or join on this field.

pkg/db/queries/payerReports.sql.go (8)

57-61: Good addition of a new FetchPayerReport query

The new SQL query correctly selects all necessary fields from the payer_reports table with appropriate filtering by ID.


63-79: Implementation looks correct

The function properly queries the database and scans all columns from the payer_reports table into the PayerReport struct.


228-249: Well-designed insert query with conflict handling

The SQL query correctly implements an insert-or-ignore pattern using the ON CONFLICT DO NOTHING clause, which prevents duplicate entries and makes the operation idempotent.


251-260: Parameter struct matches database schema

The InsertOrIgnorePayerReportParams struct correctly defines all required fields with appropriate types for the database schema.


262-274: Implementation looks correct

The function executes the insert query with the proper parameters and error handling.


276-279: Well-designed insert query with conflict handling

The SQL query correctly implements an insert-or-ignore pattern using the ON CONFLICT DO NOTHING clause on the composite key (payer_report_id, node_id), making the operation idempotent.


281-285: Parameter struct matches database schema

The InsertOrIgnorePayerReportAttestationParams struct correctly defines all required fields with appropriate types.


287-290: Implementation looks correct

The function executes the insert query with the proper parameters and error handling.

pkg/payerreport/store.go (5)

13-16: Good error definitions for parameter validation

These error variables appropriately define specific error types for validation failures when converting between uint32 and int32 types.


18-28: Clean Store struct design with proper dependency injection

The Store struct follows good design patterns by:

  1. Using dependency injection for the queries and logger dependencies
  2. Using a named logger for better log organization

30-62: Comprehensive validation and type conversion in StoreReport

The StoreReport method includes appropriate validations to ensure values fit within their database column types and properly converts between in-memory and database types.


64-77: Clean implementation of StoreAttestation

The method properly extracts the report ID, handles errors, and converts types correctly before inserting the attestation.


79-103: Good error handling in FetchReport

The FetchReport method appropriately handles database errors and conversion errors when transforming byte slices to fixed-size arrays.

Comment on lines +3 to +7
import "errors"

var (
ErrSliceNot32Bytes = errors.New("slice must be 32 bytes long")
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Error declaration formatting issue

The var declaration block needs formatting according to the linter. This appears to be causing your pipeline failures.

- import "errors"
- 
- var (
- 	ErrSliceNot32Bytes = errors.New("slice must be 32 bytes long")
- )
+ import "errors"
+ 
+ var ErrSliceNot32Bytes = errors.New("slice must be 32 bytes long")

The linting tool (gofumpt) is flagging this as improperly formatted. A single error variable should not be placed in a var block.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import "errors"
var (
ErrSliceNot32Bytes = errors.New("slice must be 32 bytes long")
)
import "errors"
var ErrSliceNot32Bytes = errors.New("slice must be 32 bytes long")
🧰 Tools
🪛 GitHub Check: Lint-Go

[failure] 5-5:
File is not properly formatted (gofumpt)

🪛 GitHub Actions: Lint

[error] 5-5: File is not properly formatted (gofumpt)

Comment on lines +157 to +162
func randomBytes32() [32]byte {
var b [32]byte
//nolint:errcheck
rand.Read(b[:])
return b
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle errors in randomBytes32 function.

The function ignores potential errors from rand.Read with the //nolint:errcheck comment. Even for temporary code, proper error handling is important for cryptographic operations, as failure could lead to predictable values or panics in production.

// Temporary function until we have a real merkle root
func randomBytes32() [32]byte {
  var b [32]byte
-  //nolint:errcheck
-  rand.Read(b[:])
+  _, err := rand.Read(b[:])
+  if err != nil {
+    // Log error and/or use a fallback strategy
+    log.Printf("Failed to generate random bytes: %v", err)
+  }
  return b
}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +36 to +44
func (p *PayerReport) ToProto() *proto.PayerReport {
return &proto.PayerReport{
OriginatorNodeId: p.OriginatorNodeID,
StartSequenceId: p.StartSequenceID,
EndSequenceId: p.EndSequenceID,
PayersMerkleRoot: p.PayersMerkleRoot[:],
PayersLeafCount: p.PayersLeafCount,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Include all fields in ToProto() method.

The ToProto() method doesn't include NodesHash and NodesCount fields from the PayerReport struct, which may lead to incomplete data when serializing/deserializing between Go and protobuf.

func (p *PayerReport) ToProto() *proto.PayerReport {
  return &proto.PayerReport{
    OriginatorNodeId: p.OriginatorNodeID,
    StartSequenceId:  p.StartSequenceID,
    EndSequenceId:    p.EndSequenceID,
    PayersMerkleRoot: p.PayersMerkleRoot[:],
    PayersLeafCount:  p.PayersLeafCount,
+   NodesHash:        p.NodesHash[:],
+   NodesCount:       p.NodesCount,
  }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (p *PayerReport) ToProto() *proto.PayerReport {
return &proto.PayerReport{
OriginatorNodeId: p.OriginatorNodeID,
StartSequenceId: p.StartSequenceID,
EndSequenceId: p.EndSequenceID,
PayersMerkleRoot: p.PayersMerkleRoot[:],
PayersLeafCount: p.PayersLeafCount,
}
}
func (p *PayerReport) ToProto() *proto.PayerReport {
return &proto.PayerReport{
OriginatorNodeId: p.OriginatorNodeID,
StartSequenceId: p.StartSequenceID,
EndSequenceId: p.EndSequenceID,
PayersMerkleRoot: p.PayersMerkleRoot[:],
PayersLeafCount: p.PayersLeafCount,
NodesHash: p.NodesHash[:],
NodesCount: p.NodesCount,
}
}

Comment on lines +46 to +61
func (p *PayerReport) ID() (ReportID, error) {
packedBytes, err := payerReportMessageHash.Pack(
p.OriginatorNodeID,
p.StartSequenceID,
p.EndSequenceID,
p.PayersMerkleRoot,
p.PayersLeafCount,
p.NodesHash,
p.NodesCount,
)
if err != nil {
return nil, err
}
// Return the keccak256 hash
return utils.HashPayerReportInput(packedBytes), nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for zero/empty values in ID() method.

The ID() method doesn't validate input fields before ABI packing. Consider adding validation to ensure that none of the byte arrays are empty or zero to prevent unexpected hashing results.

func (p *PayerReport) ID() (ReportID, error) {
+  // Validate critical fields before hashing
+  if p.PayersMerkleRoot == [32]byte{} || p.NodesHash == [32]byte{} {
+    return nil, fmt.Errorf("invalid report: empty merkle root or nodes hash")
+  }
+
  packedBytes, err := payerReportMessageHash.Pack(
    p.OriginatorNodeID,
    p.StartSequenceID,
    p.EndSequenceID,
    p.PayersMerkleRoot,
    p.PayersLeafCount,
    p.NodesHash,
    p.NodesCount,
  )
  if err != nil {
    return nil, err
  }
  // Return the keccak256 hash
  return utils.HashPayerReportInput(packedBytes), nil
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (p *PayerReport) ID() (ReportID, error) {
packedBytes, err := payerReportMessageHash.Pack(
p.OriginatorNodeID,
p.StartSequenceID,
p.EndSequenceID,
p.PayersMerkleRoot,
p.PayersLeafCount,
p.NodesHash,
p.NodesCount,
)
if err != nil {
return nil, err
}
// Return the keccak256 hash
return utils.HashPayerReportInput(packedBytes), nil
}
func (p *PayerReport) ID() (ReportID, error) {
// Validate critical fields before hashing
if p.PayersMerkleRoot == [32]byte{} || p.NodesHash == [32]byte{} {
return nil, fmt.Errorf("invalid report: empty merkle root or nodes hash")
}
packedBytes, err := payerReportMessageHash.Pack(
p.OriginatorNodeID,
p.StartSequenceID,
p.EndSequenceID,
p.PayersMerkleRoot,
p.PayersLeafCount,
p.NodesHash,
p.NodesCount,
)
if err != nil {
return nil, err
}
// Return the keccak256 hash
return utils.HashPayerReportInput(packedBytes), nil
}

Comment on lines +17 to +24
CREATE TABLE payer_report_attestations (
-- Do not reference the payer reports table since attestations may arrive before the report is stored
payer_report_id BYTEA NOT NULL,
node_id BIGINT NOT NULL,
signature BYTEA NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (payer_report_id, node_id)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consider application-level validation for data integrity.

The comment explicitly notes that foreign keys are not used, which could lead to orphaned attestations. Ensure that application-level validation is implemented to maintain data integrity.


🏁 Script executed:

#!/bin/bash
# Check if the application has proper validation logic for attestations without reports

# Look for validation code in the Store implementation
rg -A 5 "StoreAttestation" pkg/payerreport/

Length of output: 568


Add validation to prevent orphaned attestations

The StoreAttestation method in pkg/payerreport/store.go currently does:

reportID, err := attestation.Report.ID()
if err != nil {
    return err
}
return s.queries.InsertOrIgnorePayerReportAttestation(…)

Without a foreign‐key, this will insert attestations for reports that don’t exist. Add an explicit application‐level check before inserting:

– pkg/payerreport/store.go, in StoreAttestation (before calling InsertOrIgnorePayerReportAttestation)

// ensure the referenced payer report is present
if _, err := s.queries.GetPayerReport(ctx, reportID); err != nil {
    if errors.Is(err, sql.ErrNoRows) {
        return fmt.Errorf("cannot store attestation: payer report %x not found", reportID)
    }
    return err
}

This will guard against orphaned attestations and maintain data integrity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant