Skip to content

Commit

Permalink
Refactor claim and withdrawal, according to new countries logic
Browse files Browse the repository at this point in the history
  • Loading branch information
violog committed Jun 5, 2024
1 parent f4a887d commit 6bfcb1d
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 225 deletions.
4 changes: 2 additions & 2 deletions docs/spec/components/schemas/VerifyPassport.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ allOf:
properties:
proof:
type: object
format: json.RawMessage
description: JSON encoded ZK passport verification proof.
format: types.ZKProof
description: Iden3 ZK passport verification proof.
4 changes: 2 additions & 2 deletions docs/spec/components/schemas/Withdraw.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ allOf:
example: rarimo15hcd6tv7pe8hk2re7hu0zg0aphqdm2dtjrs0ds
proof:
type: object
format: json.RawMessage
description: JSON encoded ZK passport verification proof.
format: types.ZKProof
description: Iden3 ZK passport verification proof.
29 changes: 25 additions & 4 deletions internal/service/handlers/claim_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) {
return
}
if evType.Disabled {
Log(r).Infof("Attempt to claim: event type %s is disabled", event.Type)
Log(r).Infof("Event type %s is disabled", event.Type)
ape.RenderErr(w, problems.Forbidden())
return
}
Expand All @@ -55,9 +55,30 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) {
ape.RenderErr(w, problems.InternalError())
return
}
if balance == nil {
Log(r).Infof("Attempt to claim: balance nullifier=%s is disabled", event.Nullifier)
ape.RenderErr(w, problems.NotFound())
if balance == nil || balance.Country == nil {
msg := "did not verify passport"
if balance == nil {
msg = "is disabled"
}
Log(r).Infof("Balance nullifier=%s %s", event.Nullifier, msg)
ape.RenderErr(w, problems.Forbidden())
return
}

country, err := CountriesQ(r).FilterByCodes(*balance.Country).Get()
if err != nil || country == nil { // country must exist if no errors
Log(r).WithError(err).Error("Failed to get country by code")
ape.RenderErr(w, problems.InternalError())
return
}
if !country.ReserveAllowed {
Log(r).Infof("Reserve is not allowed for country=%s", *balance.Country)
ape.RenderErr(w, problems.Forbidden())
return
}
if country.Reserved >= country.ReserveLimit {
Log(r).Infof("Reserve limit is reached for country=%s", *balance.Country)
ape.RenderErr(w, problems.Forbidden())
return
}

Expand Down
2 changes: 1 addition & 1 deletion internal/service/handlers/countries_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func GetCountriesConfig(w http.ResponseWriter, r *http.Request) {
WithdrawalAllowed: c.WithdrawalAllowed,
}
// when the limit is reached, reserve is not allowed despite the config
if c.Reserved < c.ReserveLimit {
if c.Reserved >= c.ReserveLimit {
prop.ReserveAllowed = false
}
cMap[c.Code] = prop
Expand Down
259 changes: 174 additions & 85 deletions internal/service/handlers/verify_passport.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package handlers

import (
"encoding/json"
"fmt"
"math/big"
"net/http"

"github.com/ethereum/go-ethereum/common/hexutil"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/google/jsonapi"
zkptypes "github.com/iden3/go-rapidsnark/types"
"github.com/rarimo/decentralized-auth-svc/pkg/auth"
"github.com/rarimo/rarime-points-svc/internal/data"
Expand All @@ -19,6 +19,8 @@ import (
"gitlab.com/distributed_lab/logan/v3/errors"
)

const proofSelectorValue = "23073"

func VerifyPassport(w http.ResponseWriter, r *http.Request) {
req, err := requests.NewVerifyPassport(r)
if err != nil {
Expand All @@ -27,128 +29,215 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) {
return
}

nullifier := req.Data.ID
balance, errs := getAndVerifyBalanceEligibility(r, req.Data.ID, &req.Data.Attributes.Proof)
if len(errs) > 0 {
ape.RenderErr(w, errs...)
return
}

if !auth.Authenticates(UserClaims(r), auth.UserGrant(nullifier)) {
ape.RenderErr(w, problems.Unauthorized())
if balance.Country != nil {
Log(r).Debugf("Balance %s already verified", balance.Nullifier)
ape.RenderErr(w, problems.TooManyRequests())
return
}

balance, err := BalancesQ(r).FilterByNullifier(nullifier).Get()
err = EventsQ(r).Transaction(func() error {
return doPassportScanUpdates(r, *balance, req.Data.Attributes.Proof)
})
if err != nil {
Log(r).WithError(err).Error("Failed to get balance by nullifier")
Log(r).WithError(err).Error("Failed to execute transaction")
ape.RenderErr(w, problems.InternalError())
return
}

if balance == nil {
Log(r).Debug("Balance absent")
ape.RenderErr(w, problems.NotFound())
return
w.WriteHeader(http.StatusNoContent)
}

// getAndVerifyBalanceEligibility provides common logic to verify that the user
// is eligible to verify passport or withdraw. Some extra checks still exist in
// the flows.
func getAndVerifyBalanceEligibility(
r *http.Request,
nullifier string,
proof *zkptypes.ZKProof,
) (balance *data.Balance, errs []*jsonapi.ErrorObject) {

if !auth.Authenticates(UserClaims(r), auth.UserGrant(nullifier)) {
return nil, append(errs, problems.Unauthorized())
}

if !balance.ReferredBy.Valid {
Log(r).Debug("Balance inactive")
ape.RenderErr(w, problems.BadRequest(validation.Errors{"referred_by": errors.New("balance inactive")})...)
return
balance, err := BalancesQ(r).FilterByNullifier(nullifier).Get()
if err != nil {
Log(r).WithError(err).Error("Failed to get balance by nullifier")
return nil, append(errs, problems.InternalError())
}

evType := EventTypes(r).Get(evtypes.TypePassportScan, evtypes.FilterInactive)
if evType == nil {
Log(r).Debug("Passport scan event absent, disabled, hasn't start yet or expired")
ape.RenderErr(w, problems.BadRequest(validation.Errors{"passport_scan": errors.New("event disabled or absent")})...)
return
if errs = checkVerificationEligibility(r, balance); len(errs) > 0 {
return nil, errs
}

event, err := EventsQ(r).FilterByNullifier(nullifier).
FilterByType(evtypes.TypePassportScan).
FilterByStatus(data.EventOpen).Get()
// MustDecode will never panic, because of the previous logic of request validation
proof.PubSignals[zk.Nullifier] = new(big.Int).SetBytes(hexutil.MustDecode(nullifier)).String()
err = Verifier(r).VerifyProof(*proof, zk.WithProofSelectorValue(proofSelectorValue))
if err != nil {
Log(r).WithError(err).Error("Failed to get passport scan event")
ape.RenderErr(w, problems.InternalError())
return
return nil, problems.BadRequest(err)
}

if event == nil {
Log(r).Debug("Event already fulfilled or absent for user")
ape.RenderErr(w, problems.TooManyRequests())
return
return balance, nil
}

func checkVerificationEligibility(r *http.Request, balance *data.Balance) (errs []*jsonapi.ErrorObject) {
switch {
case balance == nil:
Log(r).Debug("Balance absent")
return append(errs, problems.NotFound())
case !balance.ReferredBy.Valid:
Log(r).Debug("Balance inactive")
return append(errs, problems.BadRequest(validation.Errors{
"referred_by": errors.New("user must be referred to withdraw"),
})...)
}

var proof zkptypes.ZKProof
if err := json.Unmarshal(req.Data.Attributes.Proof, &proof); err != nil {
ape.RenderErr(w, problems.BadRequest(err)...)
return
return nil
}

// doPassportScanUpdates performs all the necessary updates when the passport
// scan proof is provided. This logic is shared between verification and
// withdrawal handlers.
func doPassportScanUpdates(r *http.Request, balance data.Balance, proof zkptypes.ZKProof) error {
country, err := updateBalanceCountry(r, balance, proof)
if err != nil {
return fmt.Errorf("update balance country: %w", err)
}
if !country.ReserveAllowed || !country.WithdrawalAllowed || country.Reserved >= country.ReserveLimit {
Log(r).Infof("User %s scanned passport which country has restrictions: %+v", balance.Nullifier, country)
}

// MustDecode will never panic, because of the previous logic
proof.PubSignals[zk.Nullifier] = new(big.Int).SetBytes(hexutil.MustDecode(nullifier)).String()
if err := Verifier(r).VerifyProof(proof, zk.WithProofSelectorValue("23073")); err != nil {
ape.RenderErr(w, problems.BadRequest(err)...)
return
if err = fulfillPassportScanEvent(r, balance); err != nil {
return fmt.Errorf("fulfill passport scan event: %w", err)
}

evType = EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive)
if evType == nil {
Log(r).Debug("Referral event type is disabled or expired, not accruing points to referrer")
if err = addEventForReferrer(r, balance); err != nil {
return fmt.Errorf("add event for referrer: %w", err)
}

err = EventsQ(r).Transaction(func() (err error) {
countryCode := decodeInt(proof.PubSignals[zk.Citizenship])
if country, ok := Countries(r)[countryCode]; !ok {
err = CountriesQ(r).Insert(data.Country{Code: countryCode,
ReserveLimit: country.ReserveLimit,
ReserveAllowed: country.ReserveAllowed,
WithdrawalAllowed: country.WithdrawalAllowed})
if err != nil {
return fmt.Errorf("failed to inser new country: %w", err)
}
}
if err = BalancesQ(r).
FilterByNullifier(nullifier).
Update(map[string]any{"country": countryCode}); err != nil {
return fmt.Errorf("failed to update country: %w", err)
}
return nil
}

if evType != nil {
// ReferredBy always valid because of the previous logic
referral, err := ReferralsQ(r).Get(balance.ReferredBy.String)
if err != nil {
return fmt.Errorf("failed to get referral by ID: %w", err)
}

err = EventsQ(r).Insert(data.Event{
Nullifier: referral.Nullifier,
Type: evType.Name,
Status: data.EventFulfilled,
Meta: data.Jsonb(fmt.Sprintf(`{"nullifier": "%s"}`, nullifier)),
})
if err != nil {
return fmt.Errorf("add event for referrer: %w", err)
}
func updateBalanceCountry(r *http.Request, balance data.Balance, proof zkptypes.ZKProof) (*data.Country, error) {
country, err := getOrCreateCountry(CountriesQ(r), proof)
if err != nil {
return nil, fmt.Errorf("get or create country: %w", err)
}
if balance.Country != nil {
if *balance.Country == country.Code {
return country, nil
}
// countries mismatch is handled separately in withdrawal flow before calling
// updateBalanceCountry, so this will never happen
return nil, errors.New("countries mismatch")
}

_, err = EventsQ(r).
FilterByID(event.ID).
Update(data.EventFulfilled, nil, nil)
if err != nil {
return fmt.Errorf("failed to update passport scan event: %w", err)
}
err = BalancesQ(r).FilterByNullifier(balance.Nullifier).Update(map[string]any{
data.ColCountry: country.Code,
})
if err != nil {
return nil, fmt.Errorf("update balance country: %w", err)
}

return country, nil
}

func fulfillPassportScanEvent(r *http.Request, balance data.Balance) error {
evTypePassport := EventTypes(r).Get(evtypes.TypePassportScan, evtypes.FilterInactive)
if evTypePassport == nil {
Log(r).Debug("Passport scan event type is inactive")
return nil
}

event, err := EventsQ(r).FilterByNullifier(balance.Nullifier).
FilterByType(evtypes.TypePassportScan).
FilterByStatus(data.EventOpen).Get()
if err != nil {
return fmt.Errorf("get open passport scan event: %w", err)
}

if event == nil {
return errors.New("inconsistent state: balance has no country, event type is active, but no open event was found")
}

_, err = EventsQ(r).
FilterByID(event.ID).
Update(data.EventFulfilled, nil, nil)

return err
}

func addEventForReferrer(r *http.Request, balance data.Balance) error {
evTypeRef := EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive)
if evTypeRef == nil {
Log(r).Debug("Referral event type is inactive, not fulfilling event for referrer")
return nil
}

// ReferredBy always valid because of the previous logic
referral, err := ReferralsQ(r).Get(balance.ReferredBy.String)
if err != nil {
return fmt.Errorf("get referral by ID: %w", err)
}

return EventsQ(r).Insert(data.Event{
Nullifier: referral.Nullifier,
Type: evTypeRef.Name,
Status: data.EventFulfilled,
Meta: data.Jsonb(fmt.Sprintf(`{"nullifier": "%s"}`, balance.Nullifier)),
})
}

func getOrCreateCountry(q data.CountriesQ, proof zkptypes.ZKProof) (*data.Country, error) {
code := extractCountry(proof)
if code == "" {
return nil, errors.New("country pub signal is not decimal big integer")
}

c, err := q.FilterByCodes(code).Get()
if err != nil {
Log(r).WithError(err).Error("Failed to add referral event and update verify passport event")
ape.RenderErr(w, problems.InternalError())
return
return nil, fmt.Errorf("get country by code: %w", err)
}
if c != nil {
return c, nil
}

w.WriteHeader(http.StatusNoContent)
def, err := q.New().FilterByCodes(data.DefaultCountryCode).Get()
if err != nil {
return nil, fmt.Errorf("get default country: %w", err)
}
if def == nil {
return nil, errors.New("default country does not exist in DB")
}

c = &data.Country{
Code: code,
ReserveLimit: def.ReserveLimit,
ReserveAllowed: def.ReserveAllowed,
WithdrawalAllowed: def.WithdrawalAllowed,
}

if err = q.New().Insert(*c); err != nil {
return nil, fmt.Errorf("insert country with default values: %w", err)
}

return c, nil
}

func decodeInt(s string) string {
b, ok := new(big.Int).SetString(s, 10)
// extractCountry extracts 3-letter country code from the proof.
//
// TODO: think about some validation and case normalization, because we don't
// know what values we may encounter, resulting to applying default settings when
// we must not do it. For example, if we specify 'USA' in forbidden countries,
// but the proof contains 'usa', this situation is unpleasant.
func extractCountry(proof zkptypes.ZKProof) string {
b, ok := new(big.Int).SetString(proof.PubSignals[zk.Citizenship], 10)
if !ok {
b = new(big.Int)
}
Expand Down
Loading

0 comments on commit 6bfcb1d

Please sign in to comment.