Skip to content
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

feat(prophet): pricepred model handler #1168

Merged
merged 1 commit into from
Jan 21, 2025
Merged

feat(prophet): pricepred model handler #1168

merged 1 commit into from
Jan 21, 2025

Conversation

Pitasi
Copy link
Contributor

@Pitasi Pitasi commented Jan 14, 2025

This is a very bare future handler for the price prediction model we have today, and that essentially is an experiment to showcase the tech architecture (in fact, the date for the prediction in the request is hardcoded).

We lack some essential features, such as a way of configuring the prediction endpoint (again, hardcoded for now).

This is also our first tentative attempt at accepting a bytes array coming from solidity, mapping it into a json, and then doing the other way around for the response.

A sample Solidity smart contract used when testing:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "./async/IAsync.sol";

contract PricePredExample {
    // ID of the last future created by run().
    uint64 public lastFutureId;

    // Prices of the last future received.
    uint256 public bitcoinPrice;
    uint256 public tetherPrice;
    uint256 public uniswapPrice;

    function run() public {
        string[] memory tokens = new string[](3);
        tokens[0] = "bitcoin";
        tokens[1] = "tether";
        tokens[2] = "uniswap";
        lastFutureId = IASYNC_CONTRACT.addFuture("pricepred", abi.encode(tokens));

        // reset predictions while the future is running
        bitcoinPrice = 0;
        tetherPrice = 0;
        uniswapPrice = 0;
    }

    function cb(uint64 futureId, bytes calldata output) external {
        require(futureId == lastFutureId, "Received callback for wrong future");
        uint256[] memory tokens = abi.decode(output, (uint256[]));
        bitcoinPrice = tokens[0];
        tetherPrice = tokens[1];
        uniswapPrice = tokens[2];
    }
}

which can be tested using forge, like this:

# deploy
$ forge create --rpc-url http://localhost:8545 --private-key 0xba4ce04e9390b2011960bf2ac71977861145ff9bb90137a3f3c56bc722e972f5 src/PricePredExample.sol:PricePredExample

# schedule the future
$ cast send --private-key 0xba4ce04e9390b2011960bf2ac71977861145ff9bb90137a3f3c56bc722e972f5 --verbose $CONTRACT run()

# query the future result
$ wardend q async futures
futures:
- future:
  ...
  result:
    id: "1"
    output: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGNbn8Bhl6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACOH/9ujz5wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACa2uXz3JmIA==

# convert the output, base64 -> hex
$ echo 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGNbn8Bhl6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACOH/9ujz5wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACa2uXz3JmIA==' | base64 -d | xxd -p -c0
00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000018d6e7f01865e90000000000000000000000000000000000000000000000000000002387ffdba3cf9c000000000000000000000000000000000000000000000000026b6b97cf726620

# manually invoke the callback (as warden doesn't have the callbacks implemented yet)
$ cast send --private-key 0xba4ce04e9390b2011960bf2ac71977861145ff9bb90137a3f3c56bc722e972f5 --verbose $CONTRACT 'cb(uint64,bytes)' 1 00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000018d6e7f01865e90000000000000000000000000000000000000000000000000000002387ffdba3cf9c000000000000000000000000000000000000000000000000026b6b97cf726620

# check the bitcoin price prediction
$ cast call --private-key 0xba4ce04e9390b2011960bf2ac71977861145ff9bb90137a3f3c56bc722e972f5 --verbose 0xdEE286687809c640a7D56c34CEA7b8D3fcDaEE2c 'bitcoinPrice()(uint256)'
458207467600345628672 [4.582e20]

Summary by CodeRabbit

  • New Features

    • Added a price prediction handler to the Warden Protocol
    • Implemented AI-powered price prediction functionality with support for token-based predictions
    • Introduced advanced input decoding and output encoding mechanisms for price predictions
  • Tests

    • Added comprehensive test suite for price prediction handler
    • Implemented unit tests for input decoding, output encoding, and prediction processes
  • Improvements

    • Enhanced protocol extensibility by registering new prediction handler
    • Implemented robust error handling and response management for prediction requests

Copy link
Contributor

coderabbitai bot commented Jan 14, 2025

📝 Walkthrough

Walkthrough

The pull request introduces a new price prediction handler in the Prophet system. A new package pricepred is created with a PricePredictorSolidity struct that manages price prediction functionality. The implementation includes methods for executing predictions, decoding input, encoding output, and making HTTP requests to a prediction service. The changes also add a corresponding test file to validate the package's core functions and register the new handler in the Warden application.

Changes

File Change Summary
prophet/handlers/pricepred/pricepred.go - Added new package pricepred
- Implemented PricePredictorSolidity struct
- Created methods for prediction execution and verification
- Defined request and response types
prophet/handlers/pricepred/pricepred_test.go - Added test functions for input decoding
- Implemented tests for output encoding
- Created test for prediction functionality
warden/app/app.go - Imported new pricepred package
- Registered PricePredictorSolidity handler

Sequence Diagram

sequenceDiagram
    participant Client
    participant PricePredictorSolidity
    participant PredictionService
    
    Client->>PricePredictorSolidity: Execute(input)
    PricePredictorSolidity->>PricePredictorSolidity: decodeInput
    PricePredictorSolidity->>PredictionService: Predict(request)
    PredictionService-->>PricePredictorSolidity: Response
    PricePredictorSolidity->>PricePredictorSolidity: encodeOutput
    PricePredictorSolidity-->>Client: Return encoded prediction
Loading
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 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. (Beta)
  • @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.

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.

Copy link

Hey @Pitasi and thank you for opening this pull request! 👋🏼

It looks like you forgot to add a changelog entry for your changes. Make sure to add a changelog entry in the 'CHANGELOG.md' file.

Base automatically changed from prophet-fix-devnet to main January 15, 2025 13:51
mn13
mn13 previously approved these changes Jan 16, 2025
@mn13
Copy link
Contributor

mn13 commented Jan 20, 2025

@Pitasi can you pls merge with main so we can merge this and deploy in devnet?

This is a vary bare future handler for the price prediction model we
have today, and that essentially is an experiment to showcase the tech
architecture (in fact, the date for the prediction in the request is
hardcoded).

We lack some essential features such as a way of configuring the
prediction endpoint (again, hardcoded for now).

This is also our first tentative of accepting a bytes array coming from
solidity, mapping it into a json, and then doing the other way around
for the response.
@mn13 mn13 force-pushed the pricepred-future-handler branch from 2b458ed to 4792f38 Compare January 21, 2025 07:54
Copy link

vercel bot commented Jan 21, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
warden-help-center ⬜️ Ignored (Inspect) Visit Preview Jan 21, 2025 7:54am

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: 9

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c62973 and 4792f38.

📒 Files selected for processing (3)
  • prophet/handlers/pricepred/pricepred.go (1 hunks)
  • prophet/handlers/pricepred/pricepred_test.go (1 hunks)
  • warden/app/app.go (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
warden/app/app.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

prophet/handlers/pricepred/pricepred_test.go (2)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.


Pattern **/*_test.go: "Assess the unit test code assessing sufficient code coverage for the changes associated in the pull request"

prophet/handlers/pricepred/pricepred.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

🔇 Additional comments (1)
warden/app/app.go (1)

92-92: LGTM!

The changes correctly integrate the price prediction handler:

  1. Import the pricepred package
  2. Register the handler with the prophet system

Also applies to: 250-251

Comment on lines +36 to +73
func TestEncodeOutput(t *testing.T) {
cases := []struct {
name string
request []string
expected string
}{
{
name: "single element list",
request: []string{"bitcoin", "tether", "uniswap"},
expected: "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000018d6e7f01865e90000000000000000000000000000000000000000000000000000002387ffdba3cf9c000000000000000000000000000000000000000000000000026b6b97cf726620",
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
req := Request{
SolverInput: RequestSolverInput{
Tokens: c.request,
TargetDate: "2022-01-01",
AdversaryMode: false,
},
FalsePositiveRate: 0.01,
}
res := Response{
SolverOutput: map[string]float64{
"uniswap": 17.435131034851075,
"tether": 1.000115715622902,
"bitcoin": 45820.74676003456,
},
}

actual, err := encodeOutput(req, res)
require.NoError(t, err)

require.Equal(t, c.expected, hex.EncodeToString(actual))
})
}
}
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

Update test with dynamic date and add error scenarios.

The test has two issues:

  1. Uses a hardcoded date from 2022, which is in the past
  2. Missing test cases for error scenarios

Consider:

  1. Using a dynamic future date
  2. Adding test cases for:
    • Empty token list
    • Invalid token names
    • Zero/negative predictions
    • Missing predictions for requested tokens
 func TestEncodeOutput(t *testing.T) {
+	futureDate := time.Now().AddDate(1, 0, 0).Format("2006-01-02")
 	cases := []struct {
 		name     string
 		request  []string
+		date     string
 		expected string
+		wantErr  bool
 	}{
 		{
 			name:     "single element list",
 			request:  []string{"bitcoin", "tether", "uniswap"},
+			date:     futureDate,
 			expected: "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000018d6e7f01865e90000000000000000000000000000000000000000000000000000002387ffdba3cf9c000000000000000000000000000000000000000000000000026b6b97cf726620",
+			wantErr:  false,
 		},
+		{
+			name:     "empty token list",
+			request:  []string{},
+			date:     futureDate,
+			expected: "",
+			wantErr:  true,
+		},
 	}

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

Comment on lines +12 to +34
func TestDecodeInput(t *testing.T) {
cases := []struct {
name string
input string
expected []string
}{
{
name: "single element list",
input: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANFVEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
expected: []string{"ETH"},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
bz, err := base64.StdEncoding.DecodeString(c.input)
require.NoError(t, err)
actual, err := decodeInput(bz)
require.NoError(t, err)
require.Equal(t, c.expected, actual)
})
}
}
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

Enhance test coverage with additional test cases.

The current test suite only covers a single token. Consider adding test cases for:

  • Multiple tokens
  • Empty input
  • Invalid base64 input
  • Invalid token format
 func TestDecodeInput(t *testing.T) {
 	cases := []struct {
 		name     string
 		input    string
 		expected []string
 	}{
 		{
 			name:     "single element list",
 			input:    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANFVEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
 			expected: []string{"ETH"},
 		},
+		{
+			name:     "multiple tokens",
+			input:    "...", // Add base64 encoded input for multiple tokens
+			expected: []string{"BTC", "ETH", "USDT"},
+		},
+		{
+			name:     "empty input",
+			input:    "",
+			expected: nil,
+		},
+		{
+			name:  "invalid base64",
+			input: "invalid-base64",
+			expected: nil,
+		},
 	}

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

Comment on lines +75 to +93
func TestPredict(t *testing.T) {
//t.Skip("this test relies on external HTTP call")

ctx := context.Background()
res, err := Predict(ctx, Request{
SolverInput: RequestSolverInput{
Tokens: []string{"bitcoin", "tether", "uniswap"},
TargetDate: "2022-01-01",
AdversaryMode: false,
},
FalsePositiveRate: 0.01,
})
require.NoError(t, err)
require.Len(t, res.SolverOutput, 3)

for token, pred := range res.SolverOutput {
require.NotZero(t, pred, "prediction for %t is zero", token)
}
}
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

Mock external HTTP calls and update the test date.

The test has several issues:

  1. Makes real HTTP calls which can make tests flaky and slow
  2. Uses a hardcoded date from 2022
  3. Has a commented-out skip line instead of actively managing the test execution

Consider:

  1. Using a mock HTTP client
  2. Using a dynamic future date
  3. Properly managing test execution with environment variables

Here's a suggested implementation:

+type mockHTTPClient struct {
+    DoFunc func(req *http.Request) (*http.Response, error)
+}
+
+func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
+    return m.DoFunc(req)
+}
+
 func TestPredict(t *testing.T) {
-    //t.Skip("this test relies on external HTTP call")
+    if testing.Short() {
+        t.Skip("skipping test in short mode")
+    }
+
+    futureDate := time.Now().AddDate(1, 0, 0).Format("2006-01-02")
+    originalClient := client
+    defer func() { client = originalClient }()
+
+    mockResp := &http.Response{
+        StatusCode: http.StatusOK,
+        Body: io.NopCloser(strings.NewReader(`{
+            "solverOutput": {
+                "bitcoin": 50000,
+                "tether": 1,
+                "uniswap": 20
+            }
+        }`)),
+    }
+
+    client = &mockHTTPClient{
+        DoFunc: func(req *http.Request) (*http.Response, error) {
+            return mockResp, nil
+        },
+    }

     ctx := context.Background()
     res, err := Predict(ctx, Request{
         SolverInput: RequestSolverInput{
             Tokens:        []string{"bitcoin", "tether", "uniswap"},
-            TargetDate:    "2022-01-01",
+            TargetDate:    futureDate,
             AdversaryMode: false,
         },
         FalsePositiveRate: 0.01,
     })

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

Comment on lines +106 to +109
func (s PricePredictorSolidity) Verify(ctx context.Context, input []byte, output []byte) error {
// todo: verify output
return nil
}
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

Implement the Verify method.

The Verify method is a critical security feature that is currently not implemented. This could lead to acceptance of invalid or malicious responses.

The method should verify:

  1. Response signature if available
  2. Response format and data types
  3. Prediction values are within reasonable bounds
  4. All requested tokens are present in the response

Would you like me to generate an implementation for the Verify method?

Comment on lines +18 to +22
var URL = "https://prediction.devnet.wardenprotocol.org/task/inference/solve"

var client = http.Client{
Timeout: 3 * time.Second,
}
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

Make URL configurable and review timeout duration.

  1. The prediction service URL is hardcoded to a devnet endpoint, which limits flexibility across different environments.
  2. The 3-second timeout might be too short for some network conditions.

Consider:

  1. Making the URL configurable through environment variables or configuration
  2. Increasing the timeout or making it configurable
  3. Adding retries for transient failures
-var URL = "https://prediction.devnet.wardenprotocol.org/task/inference/solve"
+var (
+    URL = getEnvOrDefault(
+        "PREDICTION_SERVICE_URL",
+        "https://prediction.devnet.wardenprotocol.org/task/inference/solve",
+    )
+    timeout = getEnvDurationOrDefault("PREDICTION_SERVICE_TIMEOUT", 10*time.Second)
+)
 
 var client = http.Client{
-    Timeout: 3 * time.Second,
+    Timeout: timeout,
 }
+
+func getEnvOrDefault(key, defaultValue string) string {
+    if value := os.Getenv(key); value != "" {
+        return value
+    }
+    return defaultValue
+}
+
+func getEnvDurationOrDefault(key string, defaultValue time.Duration) time.Duration {
+    if value := os.Getenv(key); value != "" {
+        if duration, err := time.ParseDuration(value); err == nil {
+            return duration
+        }
+    }
+    return defaultValue
+}
📝 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
var URL = "https://prediction.devnet.wardenprotocol.org/task/inference/solve"
var client = http.Client{
Timeout: 3 * time.Second,
}
var (
URL = getEnvOrDefault(
"PREDICTION_SERVICE_URL",
"https://prediction.devnet.wardenprotocol.org/task/inference/solve",
)
timeout = getEnvDurationOrDefault("PREDICTION_SERVICE_TIMEOUT", 10*time.Second)
)
var client = http.Client{
Timeout: timeout,
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvDurationOrDefault(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
}
return defaultValue
}

Comment on lines +56 to +76
func decodeInput(input []byte) ([]string, error) {
typ, err := abi.NewType("string[]", "string[]", nil)
if err != nil {
return nil, err
}
args := abi.Arguments{
{Type: typ},
}

unpackArgs, err := args.Unpack(input)
if err != nil {
return nil, err
}

tokens, ok := unpackArgs[0].([]string)
if !ok {
return nil, fmt.Errorf("failed to unpack input")
}

return tokens, 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

Improve error handling and add token validation.

The function needs better error handling and input validation:

  1. Type assertion error message is not descriptive enough
  2. Missing validation for token strings
  3. No maximum limit on number of tokens

Consider:

 func decodeInput(input []byte) ([]string, error) {
+    const maxTokens = 100 // Prevent DoS attacks
+
     typ, err := abi.NewType("string[]", "string[]", nil)
     if err != nil {
         return nil, err
     }
     args := abi.Arguments{
         {Type: typ},
     }
 
     unpackArgs, err := args.Unpack(input)
     if err != nil {
         return nil, err
     }
 
     tokens, ok := unpackArgs[0].([]string)
     if !ok {
-        return nil, fmt.Errorf("failed to unpack input")
+        return nil, fmt.Errorf("expected []string, got %T", unpackArgs[0])
+    }
+
+    if len(tokens) > maxTokens {
+        return nil, fmt.Errorf("too many tokens: %d (max: %d)", len(tokens), maxTokens)
+    }
+
+    for i, token := range tokens {
+        if !isValidToken(token) {
+            return nil, fmt.Errorf("invalid token at index %d: %s", i, token)
+        
     }
 
     return tokens, nil
 }
+
+func isValidToken(token string) bool {
+    // Add validation logic for token strings
+    return len(token) > 0 && len(token) <= 10 && !strings.ContainsAny(token, " \t\n")
+}

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

Comment on lines +28 to +54
func (s PricePredictorSolidity) Execute(ctx context.Context, input []byte) ([]byte, error) {
tokens, err := decodeInput(input)
if err != nil {
return nil, err
}

req := Request{
SolverInput: RequestSolverInput{
Tokens: tokens,
TargetDate: "2022-01-01",
AdversaryMode: false,
},
FalsePositiveRate: 0.01,
}

res, err := Predict(ctx, req)
if err != nil {
return nil, err
}

encodedRes, err := encodeOutput(req, res)
if err != nil {
return nil, err
}

return encodedRes, nil
}
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

Remove hardcoded date and add input validation.

The Execute method has several issues:

  1. Uses a hardcoded date from 2022
  2. Lacks input validation
  3. Missing error handling for empty token list

Consider:

 func (s PricePredictorSolidity) Execute(ctx context.Context, input []byte) ([]byte, error) {
     tokens, err := decodeInput(input)
     if err != nil {
         return nil, err
     }
 
+    if len(tokens) == 0 {
+        return nil, fmt.Errorf("empty token list")
+    }
+
+    futureDate := time.Now().AddDate(1, 0, 0).Format("2006-01-02")
+
     req := Request{
         SolverInput: RequestSolverInput{
             Tokens:        tokens,
-            TargetDate:    "2022-01-01",
+            TargetDate:    futureDate,
             AdversaryMode: false,
         },
         FalsePositiveRate: 0.01,
     }
📝 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 (s PricePredictorSolidity) Execute(ctx context.Context, input []byte) ([]byte, error) {
tokens, err := decodeInput(input)
if err != nil {
return nil, err
}
req := Request{
SolverInput: RequestSolverInput{
Tokens: tokens,
TargetDate: "2022-01-01",
AdversaryMode: false,
},
FalsePositiveRate: 0.01,
}
res, err := Predict(ctx, req)
if err != nil {
return nil, err
}
encodedRes, err := encodeOutput(req, res)
if err != nil {
return nil, err
}
return encodedRes, nil
}
func (s PricePredictorSolidity) Execute(ctx context.Context, input []byte) ([]byte, error) {
tokens, err := decodeInput(input)
if err != nil {
return nil, err
}
if len(tokens) == 0 {
return nil, fmt.Errorf("empty token list")
}
futureDate := time.Now().AddDate(1, 0, 0).Format("2006-01-02")
req := Request{
SolverInput: RequestSolverInput{
Tokens: tokens,
TargetDate: futureDate,
AdversaryMode: false,
},
FalsePositiveRate: 0.01,
}
res, err := Predict(ctx, req)
if err != nil {
return nil, err
}
encodedRes, err := encodeOutput(req, res)
if err != nil {
return nil, err
}
return encodedRes, nil
}

Comment on lines +78 to +104
func encodeOutput(req Request, res Response) ([]byte, error) {
typ, err := abi.NewType("uint256[]", "", nil)
if err != nil {
log.Fatal(err)
}
args := abi.Arguments{
{
Type: typ,
Name: "SolverOutput",
Indexed: false,
},
}

tokenPreds := make([]*big.Int, len(req.SolverInput.Tokens))
for i, v := range req.SolverInput.Tokens {
decimals := big.NewFloat(1e16)
pred := big.NewFloat(res.SolverOutput[v])
tokenPreds[i], _ = big.NewFloat(0).Mul(pred, decimals).Int(nil)
}

enc, err := args.Pack(tokenPreds)
if err != nil {
return nil, err
}

return enc, nil
}
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

Fix error handling and make decimal places configurable.

Critical issues in the function:

  1. Uses log.Fatal which is inappropriate for a library function
  2. Hardcoded decimal places (1e16)
  3. Missing validation of response data
  4. No error handling for big.Float operations

Consider:

+const (
+    defaultDecimals = 16
+    maxDecimals     = 18
+)
+
 func encodeOutput(req Request, res Response) ([]byte, error) {
     typ, err := abi.NewType("uint256[]", "", nil)
     if err != nil {
-        log.Fatal(err)
+        return nil, fmt.Errorf("failed to create ABI type: %w", err)
     }
     args := abi.Arguments{
         {
             Type:    typ,
             Name:    "SolverOutput",
             Indexed: false,
         },
     }
 
     tokenPreds := make([]*big.Int, len(req.SolverInput.Tokens))
     for i, v := range req.SolverInput.Tokens {
-        decimals := big.NewFloat(1e16)
+        pred, ok := res.SolverOutput[v]
+        if !ok {
+            return nil, fmt.Errorf("missing prediction for token: %s", v)
+        }
+        if pred < 0 {
+            return nil, fmt.Errorf("negative prediction for token %s: %f", v, pred)
+        }
+
+        decimals := big.NewFloat(math.Pow10(defaultDecimals))
         pred := big.NewFloat(res.SolverOutput[v])
-        tokenPreds[i], _ = big.NewFloat(0).Mul(pred, decimals).Int(nil)
+        result, accuracy := big.NewFloat(0).Mul(pred, decimals).Int(nil)
+        if accuracy != big.Exact {
+            return nil, fmt.Errorf("loss of precision for token %s", v)
+        }
+        tokenPreds[i] = result
     }
📝 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 encodeOutput(req Request, res Response) ([]byte, error) {
typ, err := abi.NewType("uint256[]", "", nil)
if err != nil {
log.Fatal(err)
}
args := abi.Arguments{
{
Type: typ,
Name: "SolverOutput",
Indexed: false,
},
}
tokenPreds := make([]*big.Int, len(req.SolverInput.Tokens))
for i, v := range req.SolverInput.Tokens {
decimals := big.NewFloat(1e16)
pred := big.NewFloat(res.SolverOutput[v])
tokenPreds[i], _ = big.NewFloat(0).Mul(pred, decimals).Int(nil)
}
enc, err := args.Pack(tokenPreds)
if err != nil {
return nil, err
}
return enc, nil
}
const (
defaultDecimals = 16
maxDecimals = 18
)
func encodeOutput(req Request, res Response) ([]byte, error) {
typ, err := abi.NewType("uint256[]", "", nil)
if err != nil {
return nil, fmt.Errorf("failed to create ABI type: %w", err)
}
args := abi.Arguments{
{
Type: typ,
Name: "SolverOutput",
Indexed: false,
},
}
tokenPreds := make([]*big.Int, len(req.SolverInput.Tokens))
for i, v := range req.SolverInput.Tokens {
pred, ok := res.SolverOutput[v]
if !ok {
return nil, fmt.Errorf("missing prediction for token: %s", v)
}
if pred < 0 {
return nil, fmt.Errorf("negative prediction for token %s: %f", v, pred)
}
decimals := big.NewFloat(math.Pow10(defaultDecimals))
pred := big.NewFloat(res.SolverOutput[v])
result, accuracy := big.NewFloat(0).Mul(pred, decimals).Int(nil)
if accuracy != big.Exact {
return nil, fmt.Errorf("loss of precision for token %s", v)
}
tokenPreds[i] = result
}
enc, err := args.Pack(tokenPreds)
if err != nil {
return nil, err
}
return enc, nil
}

Comment on lines +130 to +168
func Predict(ctx context.Context, req Request) (Response, error) {
reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

reqBody, err := json.Marshal(req)
if err != nil {
return Response{}, err
}

httpReq, err := http.NewRequestWithContext(reqCtx, "POST", URL, bytes.NewReader(reqBody))
if err != nil {
return Response{}, err
}

httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("Content-Type", "application/json")

res, err := client.Do(httpReq)
if err != nil {
return Response{}, err
}
defer res.Body.Close()
response, err := io.ReadAll(res.Body)
if err != nil {
return Response{}, err
}

if res.StatusCode != http.StatusOK {
return Response{}, fmt.Errorf("unexpected status code: %d. Server returned error: %s", res.StatusCode, response)
}

var resResp Response
err = json.Unmarshal(response, &resResp)
if err != nil {
return Response{}, err
}

return resResp, 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

Enhance error handling and add retry mechanism.

The function has several issues:

  1. Duplicate timeout configuration
  2. No retry mechanism for transient failures
  3. Basic error handling
  4. Response body not limited in size

Consider:

+const (
+    maxResponseSize = 1 << 20 // 1MB
+    maxRetries = 3
+)
+
 func Predict(ctx context.Context, req Request) (Response, error) {
-    reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
-    defer cancel()
+    var resp Response
+    var lastErr error
 
     reqBody, err := json.Marshal(req)
     if err != nil {
         return Response{}, err
     }
 
-    httpReq, err := http.NewRequestWithContext(reqCtx, "POST", URL, bytes.NewReader(reqBody))
-    if err != nil {
-        return Response{}, err
-    }
+    for attempt := 0; attempt <= maxRetries; attempt++ {
+        if attempt > 0 {
+            // Exponential backoff
+            backoff := time.Duration(math.Pow(2, float64(attempt-1))) * time.Second
+            timer := time.NewTimer(backoff)
+            select {
+            case <-ctx.Done():
+                timer.Stop()
+                return Response{}, ctx.Err()
+            case <-timer.C:
+            }
+        }
 
-    httpReq.Header.Set("Accept", "application/json")
-    httpReq.Header.Set("Content-Type", "application/json")
+        httpReq, err := http.NewRequestWithContext(ctx, "POST", URL, bytes.NewReader(reqBody))
+        if err != nil {
+            return Response{}, err
+        }
 
-    res, err := client.Do(httpReq)
-    if err != nil {
-        return Response{}, err
-    }
-    defer res.Body.Close()
-    response, err := io.ReadAll(res.Body)
-    if err != nil {
-        return Response{}, err
-    }
+        httpReq.Header.Set("Accept", "application/json")
+        httpReq.Header.Set("Content-Type", "application/json")
 
-    if res.StatusCode != http.StatusOK {
-        return Response{}, fmt.Errorf("unexpected status code: %d. Server returned error: %s", res.StatusCode, response)
-    }
+        res, err := client.Do(httpReq)
+        if err != nil {
+            lastErr = err
+            continue
+        }
+        defer res.Body.Close()
 
-    var resResp Response
-    err = json.Unmarshal(response, &resResp)
-    if err != nil {
-        return Response{}, err
+        // Limit response size
+        limitedReader := io.LimitReader(res.Body, maxResponseSize)
+        response, err := io.ReadAll(limitedReader)
+        if err != nil {
+            lastErr = err
+            continue
+        }
+
+        if res.StatusCode != http.StatusOK {
+            lastErr = fmt.Errorf("unexpected status code: %d. Server returned error: %s", res.StatusCode, response)
+            // Don't retry on 4xx errors
+            if res.StatusCode/100 == 4 {
+                break
+            }
+            continue
+        }
+
+        if err := json.Unmarshal(response, &resp); err != nil {
+            return Response{}, fmt.Errorf("failed to unmarshal response: %w", err)
+        }
+
+        return resp, nil
     }
 
-    return resResp, nil
+    return Response{}, fmt.Errorf("max retries exceeded: %w", lastErr)
 }
📝 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 Predict(ctx context.Context, req Request) (Response, error) {
reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
reqBody, err := json.Marshal(req)
if err != nil {
return Response{}, err
}
httpReq, err := http.NewRequestWithContext(reqCtx, "POST", URL, bytes.NewReader(reqBody))
if err != nil {
return Response{}, err
}
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("Content-Type", "application/json")
res, err := client.Do(httpReq)
if err != nil {
return Response{}, err
}
defer res.Body.Close()
response, err := io.ReadAll(res.Body)
if err != nil {
return Response{}, err
}
if res.StatusCode != http.StatusOK {
return Response{}, fmt.Errorf("unexpected status code: %d. Server returned error: %s", res.StatusCode, response)
}
var resResp Response
err = json.Unmarshal(response, &resResp)
if err != nil {
return Response{}, err
}
return resResp, nil
}
const (
maxResponseSize = 1 << 20 // 1MB
maxRetries = 3
)
func Predict(ctx context.Context, req Request) (Response, error) {
var resp Response
var lastErr error
reqBody, err := json.Marshal(req)
if err != nil {
return Response{}, err
}
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
// Exponential backoff
backoff := time.Duration(math.Pow(2, float64(attempt-1))) * time.Second
timer := time.NewTimer(backoff)
select {
case <-ctx.Done():
timer.Stop()
return Response{}, ctx.Err()
case <-timer.C:
}
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", URL, bytes.NewReader(reqBody))
if err != nil {
return Response{}, err
}
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("Content-Type", "application/json")
res, err := client.Do(httpReq)
if err != nil {
lastErr = err
continue
}
defer res.Body.Close()
// Limit response size
limitedReader := io.LimitReader(res.Body, maxResponseSize)
response, err := io.ReadAll(limitedReader)
if err != nil {
lastErr = err
continue
}
if res.StatusCode != http.StatusOK {
lastErr = fmt.Errorf("unexpected status code: %d. Server returned error: %s", res.StatusCode, response)
// Don't retry on 4xx errors
if res.StatusCode/100 == 4 {
break
}
continue
}
if err := json.Unmarshal(response, &resp); err != nil {
return Response{}, fmt.Errorf("failed to unmarshal response: %w", err)
}
return resp, nil
}
return Response{}, fmt.Errorf("max retries exceeded: %w", lastErr)
}

@mn13 mn13 merged commit ab2c337 into main Jan 21, 2025
18 checks passed
@mn13 mn13 deleted the pricepred-future-handler branch January 21, 2025 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants