diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3a000f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Zero Block Global Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config.yaml b/config.yaml index 604c251..bfa2a3a 100644 --- a/config.yaml +++ b/config.yaml @@ -46,45 +46,24 @@ event_types: short_description: Short description no_auto_open: true auto_claim: true + - name: meetup_participation + title: Prove your participation by scanning QR code + reward: 5 + frequency: unlimited + description: Prove your participation by scanning QR code + short_description: Short description + auto_claim: true + qr_code_value: "qr_code_base64_string" levels: levels: - lvl: 1 threshold: 0 - referrals: 5 - withdrawal_allowed: false - - lvl: 2 - threshold: 5 - referrals: 5 - withdrawal_allowed: true - - lvl: 3 - threshold: 6 - referrals: 5 - withdrawal_allowed: true - -countries: - verification_key: "37bc75afc97f8bdcd21cda85ae7b2885b5f1205ae3d79942e56457230f1636a037cc7ebfe42998d66a3dd3446b9d29366271b4f2bd8e0d307db1d320b38fc02f" - countries: - - code: "UKR" - reserve_limit: 100000 - reserve_allowed: true - withdrawal_allowed: true - - code: "USA" - reserve_limit: 0 - reserve_allowed: false - withdrawal_allowed: false - - code: "default" - reserve_limit: 100 - reserve_allowed: true - withdrawal_allowed: true + referrals: 1 auth: addr: http://rarime-auth -broadcaster: - addr: broadcaster - sender_account: "rarimo15hcd6tv7pe8hk2re7hu0zg0aphqdm2dtjrs0ds" - verifier: verification_key_path: "./verification_key.json" allowed_age: 18 @@ -95,5 +74,5 @@ root_verifier: contract: registration_contract_address request_timeout: 10s -withdrawal: - point_price_urmo: 1000000 +sig_verifier: + verification_key: "37bc75afc97f8bdcd21cda85ae7b2885b5f1205ae3d79942e56457230f1636a037cc7ebfe42998d66a3dd3446b9d29366271b4f2bd8e0d307db1d320b38fc02f" diff --git a/docs/spec/components/schemas/CreateBalanceKey.yaml b/docs/spec/components/schemas/CreateBalanceKey.yaml index 1b62665..1b3db20 100644 --- a/docs/spec/components/schemas/CreateBalanceKey.yaml +++ b/docs/spec/components/schemas/CreateBalanceKey.yaml @@ -10,4 +10,4 @@ properties: pattern: '^0x[0-9a-fA-F]{64}$' type: type: string - enum: [ create_balance, update_balance ] + enum: [ create_balance ] diff --git a/docs/spec/components/schemas/PassportEventState.yaml b/docs/spec/components/schemas/EventClaimingState.yaml similarity index 84% rename from docs/spec/components/schemas/PassportEventState.yaml rename to docs/spec/components/schemas/EventClaimingState.yaml index 4fd519d..ae8892f 100644 --- a/docs/spec/components/schemas/PassportEventState.yaml +++ b/docs/spec/components/schemas/EventClaimingState.yaml @@ -1,5 +1,5 @@ allOf: - - $ref: '#/components/schemas/PassportEventStateKey' + - $ref: '#/components/schemas/EventClaimingStateKey' - type: object required: - attributes diff --git a/docs/spec/components/schemas/PassportEventStateKey.yaml b/docs/spec/components/schemas/EventClaimingStateKey.yaml similarity index 85% rename from docs/spec/components/schemas/PassportEventStateKey.yaml rename to docs/spec/components/schemas/EventClaimingStateKey.yaml index d630d08..0e85597 100644 --- a/docs/spec/components/schemas/PassportEventStateKey.yaml +++ b/docs/spec/components/schemas/EventClaimingStateKey.yaml @@ -10,4 +10,4 @@ properties: pattern: '^0x[0-9a-fA-F]{64}$' type: type: string - enum: [ passport_event_state ] + enum: [ event_claiming_state ] diff --git a/docs/spec/components/schemas/EventStaticMeta.yaml b/docs/spec/components/schemas/EventStaticMeta.yaml index 18fc263..a4fd8c8 100644 --- a/docs/spec/components/schemas/EventStaticMeta.yaml +++ b/docs/spec/components/schemas/EventStaticMeta.yaml @@ -69,3 +69,7 @@ properties: - not_started - expired - disabled + qr_code_value: + type: string + description: Base64-encoded QR code. Must match the code provided in event type. + example: "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABaElEQVR4AWP4//8/AyUYw" diff --git a/docs/spec/components/schemas/FulfillQREvent.yaml b/docs/spec/components/schemas/FulfillQREvent.yaml new file mode 100644 index 0000000..d585d87 --- /dev/null +++ b/docs/spec/components/schemas/FulfillQREvent.yaml @@ -0,0 +1,16 @@ +allOf: + - $ref: '#/components/schemas/FulfillQREventKey' + - type: object + x-go-is-request: true + required: + - attributes + properties: + attributes: + required: + - qr_code + type: object + properties: + qr_code: + type: string + description: Base64-encoded QR code + example: "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABaElEQVR4AWP4//8/AyUYw" diff --git a/docs/spec/components/schemas/FulfillQREventKey.yaml b/docs/spec/components/schemas/FulfillQREventKey.yaml new file mode 100644 index 0000000..7f5efd6 --- /dev/null +++ b/docs/spec/components/schemas/FulfillQREventKey.yaml @@ -0,0 +1,12 @@ +type: object +required: + - id + - type +properties: + id: + type: string + description: Event ID + example: "059c81dd-2a54-44a8-8142-c15ad8f88949" + type: + type: string + enum: [ fulfill_qr_event ] diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@join_program.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@join_program.yaml index 4b1040f..3635b66 100644 --- a/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@join_program.yaml +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@join_program.yaml @@ -35,7 +35,7 @@ post: - data properties: data: - $ref: '#/components/schemas/PassportEventState' + $ref: '#/components/schemas/EventClaimingState' 400: $ref: '#/components/responses/invalidParameter' 401: diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@verifypassport.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@verifypassport.yaml index 62955d5..e51f45d 100644 --- a/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@verifypassport.yaml +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@balances@{nullifier}@verifypassport.yaml @@ -37,7 +37,7 @@ post: - data properties: data: - $ref: '#/components/schemas/PassportEventState' + $ref: '#/components/schemas/EventClaimingState' 400: $ref: '#/components/responses/invalidParameter' 401: diff --git a/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}@qrcode.yaml b/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}@qrcode.yaml new file mode 100644 index 0000000..632b381 --- /dev/null +++ b/docs/spec/paths/integrations@geo-points-svc@v1@public@events@{id}@qrcode.yaml @@ -0,0 +1,57 @@ +patch: + tags: + - Events + summary: Fulfill QR code event + description: Fulfill QR code event + operationId: fulfillQREvent + parameters: + - in: path + name: 'id' + required: true + schema: + type: string + example: "059c81dd-2a54-44a8-8142-c15ad8f88949" + - in: header + name: Signature + description: Signature of the request + required: true + schema: + type: string + pattern: '^[a-f0-9]{64}$' + requestBody: + required: true + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/FulfillQREvent' + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/EventClaimingState' + 400: + $ref: '#/components/responses/invalidParameter' + 401: + $ref: '#/components/responses/invalidAuth' + 403: + description: This event type was disabled and cannot be fulfilled + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Errors' + 404: + $ref: '#/components/responses/notFound' + 500: + $ref: '#/components/responses/internalError' diff --git a/go.mod b/go.mod index 1b6d7d4..6ca3363 100644 --- a/go.mod +++ b/go.mod @@ -11,11 +11,14 @@ require ( github.com/go-co-op/gocron/v2 v2.2.2 github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/google/jsonapi v1.0.0 + github.com/google/uuid v1.6.0 github.com/iden3/go-rapidsnark/types v0.0.3 + github.com/labstack/gommon v0.4.0 github.com/rarimo/decentralized-auth-svc v0.0.0-20240522134350-2694eafa9509 github.com/rarimo/saver-grpc-lib v1.0.0 github.com/rarimo/zkverifier-kit v1.0.0 github.com/rubenv/sql-migrate v1.6.1 + github.com/stretchr/testify v1.9.0 gitlab.com/distributed_lab/ape v1.7.1 gitlab.com/distributed_lab/figure/v3 v3.1.4 gitlab.com/distributed_lab/kit v1.11.3 @@ -80,7 +83,6 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect @@ -104,6 +106,7 @@ require ( github.com/lib/pq v1.10.9 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect @@ -133,7 +136,6 @@ require ( github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.18.2 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect @@ -144,6 +146,8 @@ require ( github.com/tendermint/tm-db v0.6.7 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/zondax/hid v0.9.1 // indirect github.com/zondax/ledger-go v0.14.1 // indirect gitlab.com/distributed_lab/figure v2.1.2+incompatible // indirect diff --git a/go.sum b/go.sum index 4cbe6a0..e696ff5 100644 --- a/go.sum +++ b/go.sum @@ -1879,6 +1879,7 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= @@ -2282,10 +2283,12 @@ github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhA github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= diff --git a/internal/assets/migrations/001_initial.sql b/internal/assets/migrations/001_initial.sql index f73881d..c3d74a4 100644 --- a/internal/assets/migrations/001_initial.sql +++ b/internal/assets/migrations/001_initial.sql @@ -5,15 +5,15 @@ AS $$ BEGIN NEW.updated_at = EXTRACT('EPOCH' FROM NOW()); RETURN NEW; END; $$; CREATE TABLE IF NOT EXISTS balances ( - nullifier TEXT PRIMARY KEY, - amount bigint NOT NULL default 0, - created_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), - updated_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), - referred_by text UNIQUE, - level INT NOT NULL, - anonymous_id TEXT UNIQUE, - is_verified BOOLEAN NOT NULL default FALSE, - is_passport_proven BOOLEAN NOT NULL default FALSE + nullifier TEXT PRIMARY KEY, + amount bigint NOT NULL default 0, + created_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), + updated_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), + referred_by text, + level INT NOT NULL, + anonymous_id TEXT UNIQUE, + is_verified BOOLEAN NOT NULL default FALSE, + is_passport_proven BOOLEAN NOT NULL default FALSE ); CREATE INDEX IF NOT EXISTS balances_page_index ON balances (amount, updated_at) WHERE referred_by IS NOT NULL; @@ -27,8 +27,8 @@ EXECUTE FUNCTION trigger_set_updated_at(); CREATE TABLE IF NOT EXISTS referrals ( - id text PRIMARY KEY, - nullifier TEXT NOT NULL REFERENCES balances (nullifier), + id text PRIMARY KEY, + nullifier TEXT NOT NULL REFERENCES balances (nullifier), usage_left INTEGER NOT NULL DEFAULT 1 ); @@ -40,7 +40,7 @@ CREATE TYPE event_status AS ENUM ('open', 'fulfilled', 'claimed'); CREATE TABLE IF NOT EXISTS events ( id uuid PRIMARY KEY NOT NULL default gen_random_uuid(), - nullifier TEXT NOT NULL REFERENCES balances (nullifier), + nullifier TEXT NOT NULL REFERENCES balances (nullifier), type text NOT NULL, status event_status NOT NULL, created_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), diff --git a/internal/config/levels.go b/internal/config/levels.go index 3cdd9f4..5541e28 100644 --- a/internal/config/levels.go +++ b/internal/config/levels.go @@ -10,10 +10,9 @@ import ( ) type Level struct { - Level int `fig:"lvl,required"` - Threshold int `fig:"threshold,required"` - Referrals int `fig:"referrals,required"` - WithdrawalAllowed bool `fig:"withdrawal_allowed"` + Level int `fig:"lvl,required"` + Threshold int `fig:"threshold,required"` + Referrals int `fig:"referrals,required"` } type Levels map[int]Level diff --git a/internal/config/main.go b/internal/config/main.go index 64e1df9..53be374 100644 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -21,6 +21,7 @@ type Config interface { Levels() Levels Verifier() *zk.Verifier + SigVerifier() []byte } type config struct { @@ -32,9 +33,10 @@ type config struct { identity.VerifierProvider evtypes.EventTypeser - levels comfig.Once - verifier comfig.Once - getter kv.Getter + levels comfig.Once + verifier comfig.Once + sigVerifier comfig.Once + getter kv.Getter } func New(getter kv.Getter) Config { diff --git a/internal/config/sig_verifier.go b/internal/config/sig_verifier.go new file mode 100644 index 0000000..6680fca --- /dev/null +++ b/internal/config/sig_verifier.go @@ -0,0 +1,31 @@ +package config + +import ( + "encoding/hex" + "fmt" + + "gitlab.com/distributed_lab/figure/v3" + "gitlab.com/distributed_lab/kit/kv" +) + +func (c *config) SigVerifier() []byte { + return c.sigVerifier.Do(func() interface{} { + var cfg struct { + VerificationKey string `fig:"verification_key,required"` + } + + err := figure.Out(&cfg). + From(kv.MustGetStringMap(c.getter, "sig_verifier")). + Please() + if err != nil { + panic(fmt.Errorf("failed to figure out sig_verifier: %w", err)) + } + + key, err := hex.DecodeString(cfg.VerificationKey) + if err != nil { + panic(fmt.Errorf("verification_key is not a hex: %w", err)) + } + + return key + }).([]byte) +} diff --git a/internal/data/evtypes/main.go b/internal/data/evtypes/main.go index e14ae53..3acecf9 100644 --- a/internal/data/evtypes/main.go +++ b/internal/data/evtypes/main.go @@ -49,6 +49,7 @@ type EventConfig struct { Disabled bool `fig:"disabled"` ActionURL *url.URL `fig:"action_url"` Logo *url.URL `fig:"logo"` + QRCodeValue string `fig:"qr_code_value"` } func (e EventConfig) Flag() string { @@ -73,6 +74,11 @@ func (e EventConfig) Resource() resources.EventStaticMeta { return &s } + var qrValue *string + if e.QRCodeValue != "" { + qrValue = &e.QRCodeValue + } + return resources.EventStaticMeta{ Name: e.Name, Description: e.Description, @@ -85,6 +91,7 @@ func (e EventConfig) Resource() resources.EventStaticMeta { ActionUrl: safeConv(e.ActionURL), Logo: safeConv(e.Logo), Flag: e.Flag(), + QrCodeValue: qrValue, } } diff --git a/internal/data/pg/balances.go b/internal/data/pg/balances.go index 8f0f4c4..e0a5873 100644 --- a/internal/data/pg/balances.go +++ b/internal/data/pg/balances.go @@ -55,7 +55,7 @@ func (q *balances) Update(fields map[string]any) error { return nil } -// ApplyRankedPage is similar to the ApplyTo method for a page, +// applyRankedPage is similar to the pgdb.OffsetParams.ApplyTo method, // but the sorting values are hardcoded because the fields must // be sorted in opposite directions func applyRankedPage(page *pgdb.OffsetPageParams, sql squirrel.SelectBuilder) squirrel.SelectBuilder { @@ -86,6 +86,7 @@ func applyRankedPage(page *pgdb.OffsetPageParams, sql squirrel.SelectBuilder) sq func (q *balances) Page(page *pgdb.OffsetPageParams) data.BalancesQ { q.selector = applyRankedPage(page, q.selector) + q.rank = applyRankedPage(page, q.rank) return q } diff --git a/internal/service/handlers/create_balance.go b/internal/service/handlers/create_balance.go index bdbb610..7a9e671 100644 --- a/internal/service/handlers/create_balance.go +++ b/internal/service/handlers/create_balance.go @@ -68,14 +68,19 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) { return } - referral, err = ReferralsQ(r).Get(nullifier) - if err != nil || referral == nil { + referrals, err := ReferralsQ(r).FilterByNullifier(nullifier).Select() + if err != nil { Log(r).WithError(err).Error("Failed to get referral code by nullifier") ape.RenderErr(w, problems.InternalError()) return } + if len(referrals) != 1 { + Log(r).WithError(err).Error("There must be only 1 referral code") + ape.RenderErr(w, problems.InternalError()) + return + } - ape.Render(w, newBalanceResponse(*balance, referral)) + ape.Render(w, newBalanceResponse(*balance, &referrals[0])) } func prepareEventsWithRef(nullifier, refBy string, r *http.Request) []data.Event { diff --git a/internal/service/handlers/ctx.go b/internal/service/handlers/ctx.go index b3d925a..a182b98 100644 --- a/internal/service/handlers/ctx.go +++ b/internal/service/handlers/ctx.go @@ -21,8 +21,9 @@ const ( referralsQCtxKey eventTypesCtxKey userClaimsCtxKey - verifierCtxKey levelsCtxKey + verifierCtxKey + sigVerifierCtxKey ) func CtxLog(entry *logan.Entry) func(context.Context) context.Context { @@ -95,6 +96,16 @@ func Verifier(r *http.Request) *zk.Verifier { return r.Context().Value(verifierCtxKey).(*zk.Verifier) } +func CtxSigVerifier(sigVerifier []byte) func(context.Context) context.Context { + return func(ctx context.Context) context.Context { + return context.WithValue(ctx, sigVerifierCtxKey, sigVerifier) + } +} + +func SigVerifier(r *http.Request) []byte { + return r.Context().Value(sigVerifierCtxKey).([]byte) +} + func CtxLevels(levels config.Levels) func(context.Context) context.Context { return func(ctx context.Context) context.Context { return context.WithValue(ctx, levelsCtxKey, levels) diff --git a/internal/service/handlers/fulfill_qr_event.go b/internal/service/handlers/fulfill_qr_event.go new file mode 100644 index 0000000..0c87aef --- /dev/null +++ b/internal/service/handlers/fulfill_qr_event.go @@ -0,0 +1,108 @@ +package handlers + +import ( + "net/http" + + "github.com/labstack/gommon/log" + "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/internal/data/evtypes" + "github.com/rarimo/geo-points-svc/internal/service/hmacsig" + "github.com/rarimo/geo-points-svc/internal/service/requests" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func FulfillQREvent(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewFulfillQREvent(r) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + event, err := EventsQ(r).FilterByID(req.Data.ID).FilterByStatus(data.EventOpen).Get() + if err != nil { + Log(r).WithError(err).Error("Failed to get event by ID") + ape.RenderErr(w, problems.InternalError()) + return + } + if event == nil { + Log(r).Debugf("Event not found for id=%s status=%s", req.Data.ID, data.EventOpen) + ape.RenderErr(w, problems.NotFound()) + return + } + + if !auth.Authenticates(UserClaims(r), auth.UserGrant(event.Nullifier)) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + gotSig := r.Header.Get("Signature") + wantSig, err := hmacsig.CalculateQREventSignature(SigVerifier(r), event.Nullifier, event.ID, req.Data.Attributes.QrCode) + if err != nil { // must never happen due to preceding validation + Log(r).WithError(err).Error("Failed to calculate HMAC signature") + ape.RenderErr(w, problems.InternalError()) + return + } + + if gotSig != wantSig { + log.Warnf("QR event fulfillment unauthorized access: HMAC signature mismatch: got %s, want %s", gotSig, wantSig) + ape.RenderErr(w, problems.Forbidden()) + return + } + + evType := EventTypes(r).Get(event.Type, evtypes.FilterInactive) + if evType == nil { + Log(r).Infof("Event type %s is inactive", event.Type) + ape.RenderErr(w, problems.Forbidden()) + return + } + if evType.QRCodeValue != req.Data.Attributes.QrCode { + Log(r).Debugf("QR code for event %s doesn't match: got %s, want %s", event.Type, req.Data.Attributes.QrCode, evType.QRCodeValue) + ape.RenderErr(w, problems.Forbidden()) + return + } + + balance, err := BalancesQ(r).FilterByNullifier(event.Nullifier).FilterDisabled().Get() + if err != nil { + Log(r).WithError(err).Error("Failed to get balance by nullifier") + ape.RenderErr(w, problems.InternalError()) + return + } + if balance == nil { + Log(r).Infof("Balance nullifier=%s is disabled", event.Nullifier) + ape.RenderErr(w, problems.Forbidden()) + return + } + + if !evType.AutoClaim { + _, err = EventsQ(r).FilterByID(event.ID).Update(data.EventFulfilled, nil, nil) + if err != nil { + Log(r).WithError(err).Error("Failed to update event status") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, newEventClaimingStateResponse(balance.Nullifier, false)) + return + } + + if !balance.IsVerified { + Log(r).Infof("Balance nullifier=%s is not verified, fulfill or claim not allowed", event.Nullifier) + ape.RenderErr(w, problems.Forbidden()) + return + } + + err = EventsQ(r).Transaction(func() error { + event, err = claimEvent(r, event, balance) + return err + }) + if err != nil { + Log(r).WithError(err).Errorf("Failed to claim event %s and accrue %d points to the balance %s", + event.ID, evType.Reward, event.Nullifier) + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, newEventClaimingStateResponse(balance.Nullifier, true)) +} diff --git a/internal/service/handlers/get_balance.go b/internal/service/handlers/get_balance.go index a562846..ea2065a 100644 --- a/internal/service/handlers/get_balance.go +++ b/internal/service/handlers/get_balance.go @@ -43,12 +43,18 @@ func GetBalance(w http.ResponseWriter, r *http.Request) { var referral *data.Referral if req.ReferralCodes { - referral, err = ReferralsQ(r).Get(req.Nullifier) - if err != nil || referral == nil { - Log(r).WithError(err).Error("Failed to get referral by code nullifier") + referrals, err := ReferralsQ(r).FilterByNullifier(req.Nullifier).Select() + if err != nil { + Log(r).WithError(err).Error("Failed to get referral code by nullifier") ape.RenderErr(w, problems.InternalError()) return } + if len(referrals) != 1 { + Log(r).WithError(err).Error("There must be exactly 1 referral code") + ape.RenderErr(w, problems.InternalError()) + return + } + referral = &referrals[0] } ape.Render(w, newBalanceResponse(*balance, referral)) diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 16634f0..82c1c60 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -1,9 +1,6 @@ package handlers import ( - "crypto/hmac" - "crypto/sha256" - "encoding/hex" "fmt" "math/big" "net/http" @@ -17,6 +14,7 @@ import ( "github.com/rarimo/decentralized-auth-svc/pkg/auth" "github.com/rarimo/geo-points-svc/internal/data" "github.com/rarimo/geo-points-svc/internal/data/evtypes" + "github.com/rarimo/geo-points-svc/internal/service/hmacsig" "github.com/rarimo/geo-points-svc/internal/service/requests" "github.com/rarimo/geo-points-svc/resources" zk "github.com/rarimo/zkverifier-kit" @@ -43,21 +41,22 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { country = req.Data.Attributes.Country anonymousID = req.Data.Attributes.AnonymousId proof = req.Data.Attributes.Proof - - gotSig = r.Header.Get("Signature") - wantSig = calculatePassportVerificationSignature( - nil, // TODO: add correct verification key - req.Data.ID, - country, - anonymousID, - ) ) + gotSig := r.Header.Get("Signature") + wantSig, err := hmacsig.CalculatePassportVerificationSignature(SigVerifier(r), req.Data.ID, country, anonymousID) + if err != nil { // must never happen due to preceding validation + Log(r).WithError(err).Error("Failed to calculate HMAC signature") + ape.RenderErr(w, problems.InternalError()) + return + } + if gotSig != wantSig { - log.Warnf("Unauthorized access: HMAC signature mismatch: got %s, want %s", gotSig, wantSig) + log.Warnf("Passport verification unauthorized access: HMAC signature mismatch: got %s, want %s", gotSig, wantSig) ape.RenderErr(w, problems.Forbidden()) return } + if proof == nil { log.Debug("Proof is not provided: performing logic of joining program instead of full verification") } @@ -122,7 +121,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return } - ape.Render(w, newPassportEventStateResponse(req.Data.ID, nil)) + ape.Render(w, newEventClaimingStateResponse(req.Data.ID, false)) return } @@ -145,35 +144,17 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return } - ape.Render(w, newPassportEventStateResponse(req.Data.ID, event)) + ape.Render(w, newEventClaimingStateResponse(req.Data.ID, event != nil)) } -func newPassportEventStateResponse(id string, event *data.Event) resources.PassportEventStateResponse { +func newEventClaimingStateResponse(id string, isClaimed bool) resources.PassportEventStateResponse { var res resources.PassportEventStateResponse res.Data.ID = id - res.Data.Type = resources.PASSPORT_EVENT_STATE - res.Data.Attributes.Claimed = event != nil + res.Data.Type = resources.EVENT_CLAIMING_STATE + res.Data.Attributes.Claimed = isClaimed return res } -func calculatePassportVerificationSignature(key []byte, nullifier, country, anonymousID string) string { - bNull, err := hex.DecodeString(nullifier[2:]) - if err != nil { - panic(fmt.Errorf("nullifier was not properly validated as hex: %w", err)) - } - bAID, err := hex.DecodeString(anonymousID) - if err != nil { - panic(fmt.Errorf("anonymousID was not properly validated as hex: %w", err)) - } - - h := hmac.New(sha256.New, key) - msg := append(bNull, []byte(country)...) - msg = append(msg, bAID...) - h.Write(msg) - - return hex.EncodeToString(h.Sum(nil)) -} - // getAndVerifyBalanceEligibility provides shared logic to verify that the user // is eligible to verify passport or withdraw. Some extra checks still exist in // the flows. You may provide nil proof to handle its verification outside. diff --git a/internal/service/hmacsig/main.go b/internal/service/hmacsig/main.go new file mode 100644 index 0000000..ffd125d --- /dev/null +++ b/internal/service/hmacsig/main.go @@ -0,0 +1,54 @@ +package hmacsig + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + + "github.com/google/uuid" +) + +func CalculatePassportVerificationSignature(key []byte, nullifier, country, anonymousID string) (string, error) { + bNull, err := hex.DecodeString(nullifier[2:]) + if err != nil { + return "", fmt.Errorf("nullifier is not hex: %w", err) + } + + bAID, err := hex.DecodeString(anonymousID) + if err != nil { + return "", fmt.Errorf("anonymousID is not hex: %w", err) + } + + h := hmac.New(sha256.New, key) + msg := append(bNull, []byte(country)...) + msg = append(msg, bAID...) + h.Write(msg) + + return hex.EncodeToString(h.Sum(nil)), nil +} + +func CalculateQREventSignature(key []byte, nullifier, eventID, qrCode string) (string, error) { + bNull, err := hex.DecodeString(nullifier[2:]) + if err != nil { + return "", fmt.Errorf("nullifier is not hex: %w", err) + } + + bID, err := uuid.Parse(eventID) + if err != nil { + return "", fmt.Errorf("eventID is not uuid: %w", err) + } + + bQR, err := base64.StdEncoding.DecodeString(qrCode) + if err != nil { + return "", fmt.Errorf("qrCode is not base64: %w", err) + } + + h := hmac.New(sha256.New, key) + msg := append(bNull, bID[:]...) + msg = append(msg, bQR...) + h.Write(msg) + + return hex.EncodeToString(h.Sum(nil)), nil +} diff --git a/internal/service/hmacsig/main_test.go b/internal/service/hmacsig/main_test.go new file mode 100644 index 0000000..b18383c --- /dev/null +++ b/internal/service/hmacsig/main_test.go @@ -0,0 +1,28 @@ +package hmacsig + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestCalculateSignature use it to ensure matching signature on re-implementation +func TestCalculateSignature(t *testing.T) { + key := "ab6b3f7796728e0df9696c4a3eb600b49b51db9d230e94e9c67fef756d695b63" + nullifier := "0x973c253a93e8d2e6022721c6a8bd0205940b50cb478d485ca2cbc3354fae95ec" + country := "UKR" + anonymousID := "adeef82557bc0f95c8ffe38eca25e4d1d9da79ea14215ec52b4f21370dd60dbc" + + bKey, err := hex.DecodeString(key) + require.NoError(t, err) + sig, err := CalculatePassportVerificationSignature(bKey, nullifier, country, anonymousID) + require.NoError(t, err) + t.Log("Passport sig:", sig) + + eventID := "18593155-b6a3-4166-80f1-6bf4c5aeedf1" + qrCode := "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABaElEQVR4AWP4//8/AyUYw000" + sig, err = CalculateQREventSignature(bKey, nullifier, eventID, qrCode) + require.NoError(t, err) + t.Log("QR Event sig:", sig) +} diff --git a/internal/service/requests/fulfill_qr_event.go b/internal/service/requests/fulfill_qr_event.go new file mode 100644 index 0000000..2840eaf --- /dev/null +++ b/internal/service/requests/fulfill_qr_event.go @@ -0,0 +1,25 @@ +package requests + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/rarimo/geo-points-svc/resources" +) + +func NewFulfillQREvent(r *http.Request) (req resources.FulfillQrEventRequest, err error) { + id := chi.URLParam(r, "id") + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + err = newDecodeError("body", err) + return + } + + return req, validation.Errors{ + "data/id": validation.Validate(req.Data.ID, validation.Required, validation.In(id), is.UUID), + "data/type": validation.Validate(req.Data.Type, validation.Required, validation.In(resources.FULFILL_QR_EVENT)), + "data/attributes/qr_code": validation.Validate(req.Data.Attributes.QrCode, validation.Required, is.Base64), + }.Filter() +} diff --git a/internal/service/router.go b/internal/service/router.go index 332a586..4bd1d22 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -22,6 +22,7 @@ func Run(ctx context.Context, cfg config.Config) { handlers.CtxEventTypes(cfg.EventTypes()), handlers.CtxLevels(cfg.Levels()), handlers.CtxVerifier(cfg.Verifier()), + handlers.CtxSigVerifier(cfg.SigVerifier()), ), handlers.DBCloneMiddleware(cfg.DB()), ) @@ -40,6 +41,7 @@ func Run(ctx context.Context, cfg config.Config) { r.Use(handlers.AuthMiddleware(cfg.Auth(), cfg.Log())) r.Get("/", handlers.ListEvents) r.Get("/{id}", handlers.GetEvent) + r.Patch("/{id}/qrcode", handlers.FulfillQREvent) r.Patch("/{id}", handlers.ClaimEvent) }) r.Get("/balances", handlers.Leaderboard) diff --git a/internal/service/workers/nooneisforgotten/main.go b/internal/service/workers/nooneisforgotten/main.go index 7c33c32..468e2cd 100644 --- a/internal/service/workers/nooneisforgotten/main.go +++ b/internal/service/workers/nooneisforgotten/main.go @@ -48,6 +48,7 @@ func updatePassportScanEvents(db *pgdb.DB, types evtypes.Types, levels config.Le return nil } + // ensured in query that all balances are verified balances, err := pg.NewBalances(db).WithoutPassportEvent() if err != nil { return fmt.Errorf("failed to select balances without points for passport scan: %w", err) @@ -133,10 +134,7 @@ func updateReferralUserEvents(db *pgdb.DB, types evtypes.Types) error { // friends which have passport scanned, if it possible func claimReferralSpecificEvents(db *pgdb.DB, types evtypes.Types, levels config.Levels) error { evType := types.Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) - if evType == nil { - return nil - } - if !evType.AutoClaim { + if evType == nil || !evType.AutoClaim { return nil } @@ -149,21 +147,14 @@ func claimReferralSpecificEvents(db *pgdb.DB, types evtypes.Types, levels config } // we need to have maps which link nullifiers to events slice - toClaim := make([]string, 0, len(events)) nullifiers := make([]string, 0, len(events)) for _, event := range events { - toClaim = append(toClaim, event.ID) nullifiers = append(nullifiers, event.Nullifier) } - if len(toClaim) == 0 { + if len(nullifiers) == 0 { return nil } - _, err = pg.NewEvents(db).FilterByID(toClaim...).Update(data.EventClaimed, nil, &evType.Reward) - if err != nil { - return fmt.Errorf("update event status: %w", err) - } - 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) @@ -172,6 +163,26 @@ func claimReferralSpecificEvents(db *pgdb.DB, types evtypes.Types, levels config return errors.New("critical: events present, but no balances with nullifier") } + toClaim := make([]string, 0, len(events)) + // select events to claim only for verified balances + for _, event := range events { + for _, balance := range balances { + if event.Nullifier != balance.Nullifier || !balance.IsVerified { + continue + } + toClaim = append(toClaim, event.ID) + break + } + } + if len(toClaim) == 0 { + return nil + } + + _, err = pg.NewEvents(db).FilterByID(toClaim...).Update(data.EventClaimed, nil, &evType.Reward) + if err != nil { + return fmt.Errorf("update event status: %w", err) + } + for _, balance := range balances { err = handlers.DoClaimEventUpdates( levels, diff --git a/resources/model_event_claiming_state.go b/resources/model_event_claiming_state.go new file mode 100644 index 0000000..690c90c --- /dev/null +++ b/resources/model_event_claiming_state.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type EventClaimingState struct { + Key + Attributes EventClaimingStateAttributes `json:"attributes"` +} +type EventClaimingStateResponse struct { + Data EventClaimingState `json:"data"` + Included Included `json:"included"` +} + +type EventClaimingStateListResponse struct { + Data []EventClaimingState `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *EventClaimingStateListResponse) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *EventClaimingStateListResponse) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustEventClaimingState - returns EventClaimingState from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustEventClaimingState(key Key) *EventClaimingState { + var eventClaimingState EventClaimingState + if c.tryFindEntry(key, &eventClaimingState) { + return &eventClaimingState + } + return nil +} diff --git a/resources/model_event_claiming_state_attributes.go b/resources/model_event_claiming_state_attributes.go new file mode 100644 index 0000000..ac59b3a --- /dev/null +++ b/resources/model_event_claiming_state_attributes.go @@ -0,0 +1,10 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type EventClaimingStateAttributes struct { + // If passport scan event was automatically claimed + Claimed bool `json:"claimed"` +} diff --git a/resources/model_event_static_meta.go b/resources/model_event_static_meta.go index c9d2cc4..9968934 100644 --- a/resources/model_event_static_meta.go +++ b/resources/model_event_static_meta.go @@ -21,6 +21,8 @@ type EventStaticMeta struct { Logo *string `json:"logo,omitempty"` // Unique event code name Name string `json:"name"` + // Base64-encoded QR code. Must match the code provided in event type. + QrCodeValue *string `json:"qr_code_value,omitempty"` // Reward amount in points Reward int64 `json:"reward"` ShortDescription string `json:"short_description"` diff --git a/resources/model_fulfill_qr_event.go b/resources/model_fulfill_qr_event.go new file mode 100644 index 0000000..2eb379c --- /dev/null +++ b/resources/model_fulfill_qr_event.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type FulfillQrEvent struct { + Key + Attributes FulfillQrEventAttributes `json:"attributes"` +} +type FulfillQrEventRequest struct { + Data FulfillQrEvent `json:"data"` + Included Included `json:"included"` +} + +type FulfillQrEventListRequest struct { + Data []FulfillQrEvent `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *FulfillQrEventListRequest) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *FulfillQrEventListRequest) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustFulfillQrEvent - returns FulfillQrEvent from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustFulfillQrEvent(key Key) *FulfillQrEvent { + var fulfillQREvent FulfillQrEvent + if c.tryFindEntry(key, &fulfillQREvent) { + return &fulfillQREvent + } + return nil +} diff --git a/resources/model_fulfill_qr_event_attributes.go b/resources/model_fulfill_qr_event_attributes.go new file mode 100644 index 0000000..83a256e --- /dev/null +++ b/resources/model_fulfill_qr_event_attributes.go @@ -0,0 +1,10 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type FulfillQrEventAttributes struct { + // Base64-encoded QR code + QrCode string `json:"qr_code"` +} diff --git a/resources/model_resource_type.go b/resources/model_resource_type.go index 13cf998..0a668a6 100644 --- a/resources/model_resource_type.go +++ b/resources/model_resource_type.go @@ -12,9 +12,10 @@ const ( CLAIM_EVENT ResourceType = "claim_event" CREATE_BALANCE ResourceType = "create_balance" UPDATE_BALANCE ResourceType = "update_balance" + EVENT_CLAIMING_STATE ResourceType = "event_claiming_state" EVENT ResourceType = "event" EVENT_TYPE ResourceType = "event_type" + FULFILL_QR_EVENT ResourceType = "fulfill_qr_event" JOIN_PROGRAM ResourceType = "join_program" - PASSPORT_EVENT_STATE ResourceType = "passport_event_state" VERIFY_PASSPORT ResourceType = "verify_passport" ) diff --git a/verification_key.json b/verification_key.json new file mode 100644 index 0000000..dd20ba9 --- /dev/null +++ b/verification_key.json @@ -0,0 +1,114 @@ +{ + "protocol": "groth16", + "curve": "bn128", + "nPublic": 5, + "vk_alpha_1": [ + "20491192805390485299153009773594534940189261866228447918068658471970481763042", + "9383485363053290200918347156157836566562967994039712273449902621266178545958", + "1" + ], + "vk_beta_2": [ + [ + "6375614351688725206403948262868962793625744043794305715222011528459656738731", + "4252822878758300859123897981450591353533073413197771768651442665752259397132" + ], + [ + "10505242626370262277552901082094356697409835680220590971873171140371331206856", + "21847035105528745403288232691147584728191162732299865338377159692350059136679" + ], + [ + "1", + "0" + ] + ], + "vk_gamma_2": [ + [ + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634" + ], + [ + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531" + ], + [ + "1", + "0" + ] + ], + "vk_delta_2": [ + [ + "11724736712207111949019593730998401697527107348074558054433730069659723952198", + "3812269235168270343243866438625789247255652725845948713998444869065619437729" + ], + [ + "4827732069352508730960361151910999507804531389281984074977942418742354266366", + "19829576168231173419850938062665272700980637526023717533551988325909161705890" + ], + [ + "1", + "0" + ] + ], + "vk_alphabeta_12": [ + [ + [ + "2029413683389138792403550203267699914886160938906632433982220835551125967885", + "21072700047562757817161031222997517981543347628379360635925549008442030252106" + ], + [ + "5940354580057074848093997050200682056184807770593307860589430076672439820312", + "12156638873931618554171829126792193045421052652279363021382169897324752428276" + ], + [ + "7898200236362823042373859371574133993780991612861777490112507062703164551277", + "7074218545237549455313236346927434013100842096812539264420499035217050630853" + ] + ], + [ + [ + "7077479683546002997211712695946002074877511277312570035766170199895071832130", + "10093483419865920389913245021038182291233451549023025229112148274109565435465" + ], + [ + "4595479056700221319381530156280926371456704509942304414423590385166031118820", + "19831328484489333784475432780421641293929726139240675179672856274388269393268" + ], + [ + "11934129596455521040620786944827826205713621633706285934057045369193958244500", + "8037395052364110730298837004334506829870972346962140206007064471173334027475" + ] + ] + ], + "IC": [ + [ + "10192666360483664290592596578919942551110086066492318883952724472256730803820", + "16414269607305849664503811915771913694857049252985347792415527842101385400142", + "1" + ], + [ + "20625971444595044932614719182794551780522302124726430918624441680295725187752", + "17490117851824224563289176599680023655850058310712178132783815837676053983039", + "1" + ], + [ + "1338030076801971968797792027879093217260571527917578840795459782602944157758", + "8749917434966712775557214677854516282778592581150035522234448477571215192343", + "1" + ], + [ + "5345022972005066758277778432240885017365325096209847495356286631088782027276", + "1186359572429556347675893311359964459967395942075951400924149455810466619313", + "1" + ], + [ + "20623407267360268436697691230682295128261080344460020296651569355623478698252", + "17442678184122248154873092448240860567996339464661375407820900986429572319212", + "1" + ], + [ + "19499439418422721249077008031817446163753094464896889937326550971864597296313", + "9801400094305528195785692039165274183856468908125017060851568661310880535680", + "1" + ] + ] +} \ No newline at end of file