Skip to content

Commit

Permalink
Add DB handling of anonymous ID, fix bugs with the flow
Browse files Browse the repository at this point in the history
  • Loading branch information
violog committed Jun 21, 2024
1 parent 8e5078a commit 20c9432
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 30 deletions.
1 change: 1 addition & 0 deletions config-testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ levels:
withdrawal_allowed: true

countries:
verification_key: "37bc75afc97f8bdcd21cda85ae7b2885b5f1205ae3d79942e56457230f1636a037cc7ebfe42998d66a3dd3446b9d29366271b4f2bd8e0d307db1d320b38fc02f"
countries:
- code: "UKR"
reserve_limit: 100000
Expand Down
5 changes: 5 additions & 0 deletions internal/assets/migrations/004_anonymous_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE balances ADD COLUMN anonymous_id text UNIQUE;

-- +migrate Down
ALTER TABLE balances DROP COLUMN anonymous_id;
11 changes: 7 additions & 4 deletions internal/data/balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import (
)

const (
ColAmount = "amount"
ColLevel = "level"
ColCountry = "country"
ColIsPassport = "is_passport_proven"
ColAmount = "amount"
ColLevel = "level"
ColCountry = "country"
ColIsPassport = "is_passport_proven"
ColAnonymousID = "anonymous_id"
)

type Balance struct {
Expand All @@ -23,6 +24,7 @@ type Balance struct {
Level int `db:"level"`
Country *string `db:"country"`
IsPassportProven bool `db:"is_passport_proven"`
AnonymousID *string `db:"anonymous_id"`
}

type BalancesQ interface {
Expand All @@ -45,6 +47,7 @@ type BalancesQ interface {

FilterByNullifier(...string) BalancesQ
FilterDisabled() BalancesQ
FilterByAnonymousID(id string) BalancesQ
}

type WithoutPassportEventBalance struct {
Expand Down
4 changes: 4 additions & 0 deletions internal/data/pg/balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ func (q *balances) FilterDisabled() data.BalancesQ {
return q.applyCondition(squirrel.NotEq{"referred_by": nil})
}

func (q *balances) FilterByAnonymousID(id string) data.BalancesQ {
return q.applyCondition(squirrel.Eq{"anonymous_id": id})
}

func (q *balances) applyCondition(cond squirrel.Sqlizer) data.BalancesQ {
q.selector = q.selector.Where(cond)
q.updater = q.updater.Where(cond)
Expand Down
70 changes: 52 additions & 18 deletions internal/service/handlers/verify_passport.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,46 +33,77 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) {
ape.RenderErr(w, problems.BadRequest(err)...)
return
}

log := Log(r).WithFields(map[string]any{
"balance.nullifier": req.Data.ID,
"balance.anonymous_id": req.Data.Attributes.AnonymousId,
"balance.country": req.Data.Attributes.Country,
})

gotSig := r.Header.Get("Signature")
wantSig := calculatePassportVerificationSignature(
CountriesConfig(r).VerificationKey,
req.Data.ID,
req.Data.Attributes.Country,
req.Data.Attributes.AnonymousId,
var (
country = req.Data.Attributes.Country
anonymousID = req.Data.Attributes.AnonymousId
proof = req.Data.Attributes.Proof

gotSig = r.Header.Get("Signature")
wantSig = calculatePassportVerificationSignature(
CountriesConfig(r).VerificationKey,
req.Data.ID,
country,
anonymousID,
)
)

if gotSig != wantSig {
log.Warnf("Unauthorized access: HMAC signature mismatch: got %s, want %s", gotSig, wantSig)
ape.RenderErr(w, problems.Forbidden())
return
}
if req.Data.Attributes.Proof == nil {
if proof == nil {
log.Debug("Proof is not provided: performing logic of joining program instead of full verification")
}

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

byAnonymousID, err := BalancesQ(r).FilterByAnonymousID(anonymousID).Get()
if err != nil {
log.WithError(err).Error("Failed to get balance by anonymous ID")
ape.RenderErr(w, problems.InternalError())
return
}
if byAnonymousID != nil && byAnonymousID.Nullifier != balance.Nullifier {
log.Warn("Balance with the same anonymous ID already exists")
ape.RenderErr(w, problems.Conflict())
return
}

if balance.Country != nil {
if balance.IsPassportProven {
log.Debugf("Balance %s already verified", balance.Nullifier)
log.Warnf("Balance %s already verified", balance.Nullifier)
ape.RenderErr(w, problems.TooManyRequests())
return
}
if proof == nil {
log.Warnf("Balance %s tried to re-join program", balance.Nullifier)
ape.RenderErr(w, problems.TooManyRequests())
return
}

if *balance.Country != req.Data.Attributes.Country {
ape.RenderErr(w, problems.BadRequest(validation.Errors{
"country": fmt.Errorf("country mismatch: got %s, joined program with %s", req.Data.Attributes.Country, *balance.Country),
})...)
var balAID string
if balance.AnonymousID != nil {
balAID = *balance.AnonymousID
}

err = validation.Errors{
"data/attributes/country": validation.Validate(*balance.Country, validation.Required, validation.In(country)),
"data/attributes/anonymous_id": validation.Validate(anonymousID, validation.Required, validation.In(balAID)),
}.Filter()
if err != nil {
ape.RenderErr(w, problems.BadRequest(err)...)
return
}

Expand All @@ -90,7 +121,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) {
}

err = EventsQ(r).Transaction(func() error {
return doPassportScanUpdates(r, *balance, req.Data.Attributes.Country, true)
return doPassportScanUpdates(r, *balance, country, anonymousID, proof != nil)
})
if err != nil {
log.WithError(err).Error("Failed to execute transaction")
Expand Down Expand Up @@ -195,8 +226,8 @@ func checkVerificationEligibility(r *http.Request, balance *data.Balance) (errs
// 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, countryCode string, proven bool) error {
country, err := updateBalanceCountry(r, balance, countryCode, proven)
func doPassportScanUpdates(r *http.Request, balance data.Balance, countryCode, anonymousID string, proven bool) error {
country, err := updateBalanceCountry(r, balance, countryCode, anonymousID, proven)
if err != nil {
return fmt.Errorf("update balance country: %w", err)
}
Expand Down Expand Up @@ -247,7 +278,7 @@ func doPassportScanUpdates(r *http.Request, balance data.Balance, countryCode st
return nil
}

func updateBalanceCountry(r *http.Request, balance data.Balance, code string, proven bool) (*data.Country, error) {
func updateBalanceCountry(r *http.Request, balance data.Balance, code, anonymousID string, proven bool) (*data.Country, error) {
country, err := getOrCreateCountry(CountriesQ(r), code)
if err != nil {
return nil, fmt.Errorf("get or create country: %w", err)
Expand All @@ -261,7 +292,10 @@ func updateBalanceCountry(r *http.Request, balance data.Balance, code string, pr
return nil, errors.New("countries mismatch")
}

toUpd := map[string]any{data.ColCountry: country.Code}
toUpd := map[string]any{
data.ColCountry: country.Code,
data.ColAnonymousID: anonymousID,
}
if proven {
toUpd[data.ColIsPassport] = true
}
Expand Down
11 changes: 7 additions & 4 deletions internal/service/requests/verify_passport.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ var (
nullifierRegexp = regexp.MustCompile("^0x[0-9a-fA-F]{64}$")
hex32bRegexp = regexp.MustCompile("^[0-9a-f]{64}$")
// endpoint is hardcoded to reuse handlers.VerifyPassport
verifyPassportPathRegexp = regexp.MustCompile("^/v1/public/balances/0x[0-9a-fA-F]{64}/verifypassport$")
verifyPassportPathRegexp = regexp.MustCompile("^/integrations/rarime-points-svc/v1/public/balances/0x[0-9a-fA-F]{64}/verifypassport$")
joinProgramPathRegexp = regexp.MustCompile("^/integrations/rarime-points-svc/v1/public/balances/0x[0-9a-fA-F]{64}/join_program$")
)

func NewVerifyPassport(r *http.Request) (req resources.VerifyPassportRequest, err error) {
Expand Down Expand Up @@ -50,9 +51,11 @@ func NewVerifyPassport(r *http.Request) (req resources.VerifyPassportRequest, er
"data/type": val.Validate(req.Data.Type,
val.Required,
val.In(resources.VERIFY_PASSPORT)),
"data/attributes/anonymous_id": val.Validate(attr.AnonymousId, val.Required, val.Match(hex32bRegexp)),
"data/attributes/country": val.Validate(attr.Country, val.Required, val.In(provingCountry)),
"data/attributes/proof": val.Validate(attr.Proof, val.When(verifyPassportPathRegexp.MatchString(r.URL.Path), val.Required)),
"data/attributes/anonymous_id": val.Validate(attr.AnonymousId, val.Required, val.Match(hex32bRegexp)),
"data/attributes/country": val.Validate(attr.Country, val.Required, val.In(provingCountry), is.CountryCode3),
"data/attributes/proof": val.Validate(attr.Proof,
val.When(verifyPassportPathRegexp.MatchString(r.URL.Path), val.Required),
val.When(joinProgramPathRegexp.MatchString(r.URL.Path), val.Nil)),
"data/attributes/proof/proof": val.Validate(proof.Proof, val.When(attr.Proof != nil, val.Required)),
"data/attributes/proof/pub_signals": val.Validate(proof.PubSignals, val.When(attr.Proof != nil, val.Required, val.Length(22, 22))),
}.Filter()
Expand Down
5 changes: 1 addition & 4 deletions internal/service/workers/nooneisforgotten/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,7 @@ func claimReferralSpecificEvents(db *pgdb.DB, types evtypes.Types, levels config
return nil
}

balances, err := pg.NewBalances(db).
FilterByNullifier(nullifiers...).
FilterDisabled().
Select()
balances, err := pg.NewBalances(db).FilterByNullifier(nullifiers...).Select()
if err != nil {
return fmt.Errorf("failed to select balances for claim passport scan event: %w", err)
}
Expand Down

0 comments on commit 20c9432

Please sign in to comment.