Skip to content
Draft
76 changes: 76 additions & 0 deletions pkg/adminService/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,19 @@ type ResourceProviderListItem struct {
ResourceProvider string `json:"resource_provider"`
}

type ResourceProviderInAllowListResponse struct {
InAllowList bool `json:"inAllowList"`
}

type ResourceProviderInTestListResponse struct {
InTestList bool `json:"inTestList"`
}

type AdminServiceClient interface {
GetAllowList() ([]ResourceProviderListItem, error)
GetTestList() ([]ResourceProviderListItem, error)
IsRPonAllowList(resourceProvider string) (ResourceProviderInAllowListResponse, error)
IsRPonTestList(resourceProvider string) (ResourceProviderInTestListResponse, error)
}

type AdminServiceClientOptions struct {
Expand Down Expand Up @@ -114,3 +124,69 @@ func (a *adminServiceClient) GetAllowList() ([]ResourceProviderListItem, error)

return response, nil
}

func (a *adminServiceClient) IsRPonAllowList(resourceProvider string) (ResourceProviderInAllowListResponse, error) {
isRpOnAllowListUrl := a.clientOptions.BaseURL + "/api/v1/allow-list/contains/" + resourceProvider

req, err := http.NewRequest("GET", isRpOnAllowListUrl, nil)
if err != nil {
log.Error().Err(err).Msg("Unable to build admin service rp contained on allow list request")
return ResourceProviderInAllowListResponse{}, err
}

req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", a.clientOptions.ApiKey))
resp, err := a.client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Failed admin service call to check if RP is on allow list request")
return ResourceProviderInAllowListResponse{}, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Failed reading get rp contained on allow list response")
return ResourceProviderInAllowListResponse{}, err
}

var response ResourceProviderInAllowListResponse
err = json.Unmarshal(body, &response)
if err != nil {
log.Error().Err(err).Msg("Failed unmarshelling get rp contained on allow list response")
return ResourceProviderInAllowListResponse{}, err
}

return response, nil
}

func (a *adminServiceClient) IsRPonTestList(resourceProvider string) (ResourceProviderInTestListResponse, error) {
isRpOnAllowListUrl := a.clientOptions.BaseURL + "/api/v1/test-list/contains/" + resourceProvider

req, err := http.NewRequest("GET", isRpOnAllowListUrl, nil)
if err != nil {
log.Error().Err(err).Msg("Unable to build admin service rp contained on test list request")
return ResourceProviderInTestListResponse{}, err
}

req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", a.clientOptions.ApiKey))
resp, err := a.client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Failed admin service call to check if RP is on test list request")
return ResourceProviderInTestListResponse{}, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Failed reading get rp contained on test list response")
return ResourceProviderInTestListResponse{}, err
}

var response ResourceProviderInTestListResponse
err = json.Unmarshal(body, &response)
if err != nil {
log.Error().Err(err).Msg("Failed unmarshelling get rp contained on test list response")
return ResourceProviderInTestListResponse{}, err
}

return response, nil
}
5 changes: 5 additions & 0 deletions pkg/data/enums.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var AgreementState = []string{
"TimeoutMatch",
"TimeoutExecution",
"TimeoutDownload",
"PendingTesting",
}

// PaymentReason corresponds to PaymentReason in TypeScript
Expand Down Expand Up @@ -87,6 +88,10 @@ func GetDefaultAgreementState() uint8 {
return GetAgreementStateIndex("DealNegotiating")
}

func GetTestPendingAgreementState() uint8 {
return GetAgreementStateIndex("PendingTesting")
}

func IsActiveAgreementState(itemType uint8) bool {
return itemType == GetAgreementStateIndex("DealNegotiating") || itemType == GetAgreementStateIndex("DealAgreed")
}
Expand Down
12 changes: 12 additions & 0 deletions pkg/data/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ func GetResourceOfferContainer(
}
}

func GetTestListResourceOfferContainer(
resourceOffer ResourceOffer,
) ResourceOfferContainer {
return ResourceOfferContainer{
ID: resourceOffer.ID,
DealID: "",
ResourceProvider: resourceOffer.ResourceProvider,
State: GetTestPendingAgreementState(),
ResourceOffer: resourceOffer,
}
}

func GetDealContainer(
deal Deal,
) DealContainer {
Expand Down
110 changes: 110 additions & 0 deletions pkg/solver/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type SolverEventType string
const (
JobOfferAdded SolverEventType = "JobOfferAdded"
ResourceOfferAdded SolverEventType = "ResourceOfferAdded"
TestListResourceOfferAdded SolverEventType = "TestListResourceOfferAdded"
ResourceOfferRemoved SolverEventType = "ResourceOfferRemoved"
DealAdded SolverEventType = "DealAdded"
JobOfferStateUpdated SolverEventType = "JobOfferStateUpdated"
Expand Down Expand Up @@ -610,6 +611,83 @@ func (controller *SolverController) addResourceOffer(resourceOffer data.Resource
return ret, nil
}

func (controller *SolverController) addTestResourceOffer(resourceOffer data.ResourceOffer) (*data.ResourceOfferContainer, error) {
id, err := data.GetResourceOfferID(resourceOffer)
if err != nil {
return nil, err
}
resourceOffer.ID = id

// Check if we have an unmatched resource offer for the resource provider
existingOffers, err := controller.store.GetResourceOffers(store.GetResourceOffersQuery{
ResourceProvider: resourceOffer.ResourceProvider,
NotMatched: true,
PendingTesting: true,
})
if len(existingOffers) > 0 {
controller.log.Warn().
Str("address", resourceOffer.ResourceProvider).
Msg("resource provider posted a test list resource offer when an unmatched offer already exists")
// TODO(bgins) Return error to resource provider
// The resource provider currently crashes when an error is returned. Once we update the
// resource provider to selectively handle errors, we should return one here.
return nil, nil
}

// Check the resource provider's ETH balance
balance, err := controller.web3SDK.GetBalance(resourceOffer.ResourceProvider)
if err != nil {
return nil, fmt.Errorf("failed to retrieve ETH balance for resource provider: %v", err)
}
// Convert InstructionPrice from ETH to Wei
requiredBalanceWei := web3.EtherToWei(REQUIRED_BALANCE_IN_WEI) // 0.0006 based on the required balance for a job

// If the balance is less than the required balance, don't add the resource offer
if balance.Cmp(requiredBalanceWei) < 0 {
controller.log.Error().Err(err).
Str("addresss", resourceOffer.ResourceProvider).
Str("balance", balance.String()).
Str("requiredBalance", requiredBalanceWei.String()).
Msg("resource provider does not have enough ETH to post test resource offer")
return nil, nil
}

// required LP balance
requiredBalanceLp := web3.EtherToWei(float64(resourceOffer.DefaultPricing.InstructionPrice)) // based on the required LP balance for a job
balanceLp, err := controller.web3SDK.GetLPBalance(resourceOffer.ResourceProvider)
if err != nil {
controller.log.Error().Err(err).Msg("failed to retrieve LP balance for resource provider")
return nil, nil
}
if balanceLp.Cmp(requiredBalanceLp) < 0 {
controller.log.Error().Err(err).
Str("addresss", resourceOffer.ResourceProvider).
Str("balance", balanceLp.String()).
Str("requiredBalance", requiredBalanceLp.String()).
Msg("resource provider does not have enough LP to post test resource offer")
return nil, nil
}

controller.log.Info().Str("cid", resourceOffer.ID).
Str("address", resourceOffer.ResourceProvider).
Any("test resourceOffer", resourceOffer).
Msg("adding test list resource offer")

//TODO: figure out how you want to track this
//metricsDashboard.TrackNodeInfo(resourceOffer)

ret, err := controller.store.AddResourceOffer(data.GetTestListResourceOfferContainer(resourceOffer))
if err != nil {
return nil, err
}

controller.writeEvent(SolverEvent{
EventType: TestListResourceOfferAdded,
ResourceOffer: ret,
})
return ret, nil
}

// Remove resource offers in an unmatched DealNegotiating[0] state
func (controller *SolverController) removeUnmatchedResourceOffers(resourceProviderID string) error {
controller.log.Info().Str("address", resourceProviderID).Msg("remove unmatched resource offers")
Expand Down Expand Up @@ -870,3 +948,35 @@ func (controller *SolverController) getAllowList() ([]string, error) {

return allowList, nil
}

func (controller *SolverController) isRpOnAllowList(resourceProvider string) (bool, error) {
client, err := adminService.NewAdminServiceClient(controller.options.AdminService)
if err != nil {
controller.log.Error().Err(err).Msg("failed to create admin service client")
return false, err
}

response, err := client.IsRPonAllowList(resourceProvider)
if err != nil {
controller.log.Error().Err(err).Msg("failed to check if RP is on allow list thru admin service")
return false, err
}

return response.InAllowList, nil
}

func (controller *SolverController) isRpOnTestList(resourceProvider string) (bool, error) {
client, err := adminService.NewAdminServiceClient(controller.options.AdminService)
if err != nil {
controller.log.Error().Err(err).Msg("failed to create admin service client")
return false, err
}

response, err := client.IsRPonTestList(resourceProvider)
if err != nil {
controller.log.Error().Err(err).Msg("failed to check if RP is on test list thru admin service")
return false, err
}

return response.InTestList, nil
}
49 changes: 41 additions & 8 deletions pkg/solver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,29 +569,46 @@ func (server *solverServer) addResourceOffer(resourceOffer data.ResourceOffer, r
return nil, fmt.Errorf("resource provider address does not match signer address")
}

// Boolean flag representing whether the RP is on the testing list i.e. needs to prove they can run a job before joining the network
var isOnTestList = false

// Resource provider must be in allowlist when enabled
if server.options.AccessControl.EnableResourceProviderAllowlist {
var allowedProviders []string

// Feature Flag to control where the allow list surfaces from
if server.controller.options.AdminService.EnableAdminService {
allowedProviders, err = server.controller.getAllowList()
// Compute whether the RP is on the test list
isOnTestList, err = server.controller.isRpOnTestList(signerAddress)
if err != nil {
server.log.Error().Err(err).Msgf("Unable to load resource provider allowlist from Admin Service")
server.log.Error().Err(err).Msgf("Unable to test if resource provider allowlist from Admin Service")
return nil, err
}

// If the RP is on the test list, we will post its offer but set it so that it's pending a test (i.e. proving it can run a job) before it can join the network
if isOnTestList {
resourceOfferContainer, err := server.addTestListResourceOffer(resourceOffer)
if err != nil {
server.log.Error().Err(err).Msg("Error adding resource offer for test list")
}

server.log.Info().Str("address", resourceOffer.ResourceProvider).Any("Resource Offer", resourceOfferContainer).Msg("resource provider is in test list and posted TestPending Offer")
// Return an error here to notify the RP that it needs to undergo testing
return nil, errors.New("resource provider is on test list, your machine will under go automated testing before it will be allowed to post an official resource offer on the network")
}
} else {
allowedProviders, err = server.store.GetAllowedResourceProviders()
var allowedProviders []string
allowedProviders, err := server.store.GetAllowedResourceProviders()
if err != nil {
server.log.Error().Err(err).Msgf("Unable to load resource provider allowlist from DB")
return nil, err
}
}

if !slices.Contains(allowedProviders, resourceOffer.ResourceProvider) {
server.log.Debug().Str("address", resourceOffer.ResourceProvider).Msg("resource provider not in allowlist")
return nil, errors.New("resource provider not in beta program, request beta program access here: https://forms.gle/XaE3rRuXVLxTnZto7")
if !slices.Contains(allowedProviders, resourceOffer.ResourceProvider) {
server.log.Debug().Str("address", resourceOffer.ResourceProvider).Msg("resource provider not in allowlist")
return nil, errors.New("resource provider not in beta program, request beta program access here: https://forms.gle/XaE3rRuXVLxTnZto7")
}
}

}

if server.options.AccessControl.EnableVersionCheck {
Expand Down Expand Up @@ -621,6 +638,22 @@ func (server *solverServer) addResourceOffer(resourceOffer data.ResourceOffer, r
return server.controller.addResourceOffer(resourceOffer)
}

func (server *solverServer) addTestListResourceOffer(resourceOffer data.ResourceOffer) (*data.ResourceOfferContainer, error) {
offerRecent := isTimestampRecent(resourceOffer.CreatedAt, server.options.AccessControl.OfferTimestampDiffSeconds*1000)
if !offerRecent {
server.log.Debug().Str("cid", resourceOffer.ID).Str("address", resourceOffer.ResourceProvider).Msg("resource offer rejected because timestamp was not recent")
return nil, errors.New("resource offer rejected because CreatedAt time is not recent, check your computer's time settings and network connection")
}

err := data.CheckResourceOffer(resourceOffer)
if err != nil {
server.log.Error().Err(err).Msg("Error checking resource offer")
return nil, err
}

return server.controller.addTestResourceOffer(resourceOffer)
}

func (server *solverServer) addResult(results data.Result, res corehttp.ResponseWriter, req *corehttp.Request) (*data.Result, error) {
vars := mux.Vars(req)
id := vars["id"]
Expand Down
7 changes: 7 additions & 0 deletions pkg/solver/store/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ func (store *SolverStoreDatabase) GetResourceOffers(query store.GetResourceOffer
data.GetAgreementStateIndex("DealAgreed"),
})
}

if query.PendingTesting {
q = q.Where("state IN (?)", []uint8{
data.GetAgreementStateIndex("PendingTesting"),
})
}

if query.OrderOldestFirst {
q = q.Order("created_at ASC")
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/solver/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type GetResourceOffersQuery struct {

// Sort resource offers oldest first
OrderOldestFirst bool `json:"order_oldest_first"`

PendingTesting bool `json:"pending_test"`
}

type GetDealsQuery struct {
Expand Down
Loading