Architecture Description

The goal of this document is to outline the interactions of all the pieces involved in the current iteration of the protocol. It aims to fix interaction patterns of certain subsystems without finalising decisions on implementation choices. This should also make Phase 0.5 more understandable for everybody.

Prerequisites

We expect a certain baseline understanding of what we are doing in general (Building software systems for Stores which sell physical Goods. Currently in a physical location, meaning not e-commerce. If you are new to this, you might want to read our introduction blog post).

Creating a Store

The first thing a merchant needs to do is the creation of their store itself. This is done by minting NFT since the Store Registry is an extension of EIP721. It therefore supplies the standard conformant mint function.

The storeId can be chosen by the caller but already taken IDs will lead to a revert.

Adding Users and Logging in

Next, we introduce the concept of Users, how they are registered with a Store and how they get access to the Listing and Inventory. For this interaction flow, we differentiate Users into Clerks and Admins.

  • Registered are all Users which have a Wallet Address (WA) assigned to the Store Registry.

  • Admins can add or remove Clerks

  • registered Users can authorise KeyCards (KC) which give them access to the Store

Note

There is only one Owner, which also counts as an Admin. Only the Owner can turn Clerks into Admins and vice versa.

Onboarding a Clerk

To add a new Clerk, an Admin creates a Invite Secret IS and a corresponding Invite Verifier IV via their Client software and sends the Link containing the IS to the Clerk via some side-channel (for example E-Mail or Signal). Its public part, the IV, is submitted to the Store Registry.

  • IS and IV are the secret and public part of an ECDSA key-pair.
    • (While this processes could be modelled with a hash-function, we need to account for what is known as front running in Ethereum and similar systems, where the inputs to a contract are revealed to the public. Using signatures instead ensures only the Clerk had possession of the IS)

  • The IS is single use.

  • All Admins can see all IVs for their Store.

When the Clerk opens the Link, they are presented with a registration page. The main goal here is to connect with MetaMask to get the WA of the new Clerk. We could also collect other metadata about the Clerk at this point, like their name, an avatar or their e-mail. We could also do a reverse-lookup of an associated ENS address (e.g. my WA maps to cptx.eth) but this might have to wait until a later stage.

The IS is redeemed by calling the contract function redeemInvite, which changes the invites status for the Admins. It checks the signature by using ecrecover and checks if that returns a valid IP. Afterwards this process finally adds the new WA to the Relay configuration, which completes the registration of the Clerk.

The “Adding a Clerk” flowchart shows the interaction between the different actors.

@startuml
actor Clerk as C
actor Admin as A
database Relay as R
queue "Etherum Chain" as B

== Add User ==
A --> A ++: Wants to allow Clerk to do Clerk things.
note left:Here Clerk and Admin are synonymous\nwith different instances of the store app.

A -> A: Generate a fresh invite keypair consisting of: \nInvite _Secret_ (**IS**) and Invite _Verifier_ (**IV**)
note right of A: Conceptually this process could be a hashing operation.\nTo protect against front-running a pre-image, we are using a keypair here.\nIS is the secret and IV is the public part.

A -> B ++: Add (**IV**) to StoreReg
B --> B: Check if **IV** is already taken
B --> B: Store **IV** for one-time use
B--> A --: Ok

A --> C: Send link containing **IS**
deactivate A

C -> C++: Opens **IS** link to view Registration page

C -> C ++ #a00: Connect with MetaMask
C --> C: allow connection
C --> C --: return WalletAddress **WA**

C --> C: compute S = sign_typedData_v4(IS, WA)

C --> B ++: Reddem by submitting signed registration info to StoreReg

B --> B: check signature against valid **IV**s
alt is invalid
B --> C: Error: invite expired
else is valid
B --> B: Invalidate **IV**
B --> B: Update StoreRegistry with new Clerk **WA**
B --> C --: Ok
end

== Clerk SetProfileInfo ==

C --> C : Generate KeyCard(**KC**)
note right of C: See next section for details
C -> R ++: EnrollKeyCard{KC.public,WA}
R --> R: check on-chain data, etc..
note left of R: assuming happy path
R --> C --: Ok

C -> R ++: Start Session using KC
R --> C: Ok

C --> C: fill in profile information

C -> R: send UpdateProfileRequest{name, avatar, ...}
R --> R: update User record
R --> C: Ok

@endump

“Adding a Clerk” flowchart

Authorizing Access

To gain access to the Store, the Clerk needs to authorise a KeyCard (KC) using their WA. The main goal of the KC is to reduce the number of interactions with the Wallet. The HTTP Request for this is defined in Enroll KeyCard.

  1. Clerk generates a new asymmetric key-pair, which represents the KC. The KC is stored in the Store page and is short-lived.

  2. The public part is signed with the WA of the Clerk, prompting a Wallet interaction

  3. A request containing KC.public, StoreId and the Signature from (2) is sent to a corresponding Relay.

  4. The Relay checks the Signature and if valid, calls hasAtLeastAccess and checks if the WA from the request is part of the list.

  5. If everything does check, the KC is added to the list of allowed Users, which can then be used to initiate remote procedure call (RPC) connections to all Relays.

Logging in

The Login mechanism is necessary for elevated RPC commands that are not public, like changing the Inventory of the Store.

The “Logging in” flowchart shows the interaction between the different systems.

@startuml
actor Clerk as C
actor Admin as A
database Relay as R
queue "Etherum Chain" as B

== KeyCard Enrolment ==

C -> C ++: Generate new keypair for KeyCard (**KC**)

C -> C ++ #a00: Sign **KC**.public with __Secret__ of **WA**
C --> C: Prompt Wallet
C --> C --: Return Signature S

C -> R ++: Send EnrollKeyCard{StoreID, KC.public, S}

R --> R: extract **WA** from S via ecrecover()

R -> B ++: Check StoreRegistry by calling `hasAtLeastAccess`

B --> R --: True if **WA** does have access

alt Is Not Allowed

R -> A: Send Warning notification

R --> C: Error

else Is Allowed

R -> R: Mark **KC**.public as OK for Login

R --> C --: OK

C --> C: save **KC** in local storage

end

deactivate C

== Login ==

activate C
C -> R ++: Open RPC connection

C --> C ++: Begins Store session\n(i.e. Request Listing, Build Cart, ...)

C -> R ++ #00a : LoginRequest{**KC**.public}

R -> R: Check **KC** is allowed and remember

R -> R: Roll **Ch**allenge and remember

R --> C: Send **Ch**

C -> C: Sign **Ch** with **KC**.secret

C -> R: Send response {Ch, Sig}

R -> R: Check **Ch** is correct

R -> R: Check signature using previously claimed **KC**.public

alt Checks did not pass
R --> C: Error
R -> R: Close connection to Clerk

else Checks passed
R -> R: Unlock Session for Store access
R --> C --: Ok
end


== Do //Clerk Things//™ ==

C -> R ++ #0a0: Request listing
R -> R: check session state
R -> R: process request
...
R --> C --: Response...

...

C -> R ++ #0a0: Write Events
R -> R: check session state
R -> R: process request
...
R --> C --: Response...

@endump

“Logging in” flowchart

Store and Relay synchronization

Note

Our working assumption is that Relays and their Clients will build the Listing and Inventory with Eventual Consistency. Meaning, all changes to a Listing will be individual Events. To gain the current state of a Listing, all Events are replayed and applied locally.

Items in the Store: Retrieving the Listing

Now that the Clerk can do things in the Store, let’s discuss the first common action: display items in the Store, also known as: the Listing.

  • The Relay keeps track of which Events have been acknowledged by a KeyCard and which have not.

  • Once the Store connects to the Relay, the Relay will send all Events that have not been acknowledged yet.

  • Store should verify listing state against on-chain data

The “Retrieving the Listing” flowchart shows the interaction between the different systems.

@startuml
actor Clerk as C
boundary "Store Page" as S
database Relay as R
queue "Etherum Chain" as B
C -> S ++: Opens Page

    
alt Listing is up2date
    S --> C: RenderPage(Listing)
else Listing expired
     S -> B ++: LookupRelays(Store)
     B --> S --: Relays: [X, Y, ...]

     S -> R ++: Connect(X) and Authenticate(KC)
     
     R --> R: check **KC** validity
     R --> R: load lastAckedStoreSeq
     R --> R: buffer Events where seq > lastAcked 

     note left of R: TODO: this could be optimized into returning\nthe reduced state with the subscribe result\ninstead of replaying the full event log...

loop while len(buffer) > 0
     R --> R: Es = [E1, E2, ...] = take(buffer, N)
     R --> S: EventPushReq(Es)
     S -> S: applyEvent(E)
     S --> R: Ok
     R --> R: lastAckedStoreSeq = Es[-1].seq
     R --> R: check if there are new events and fill buffer accordingly
     deactivate R
     ...
end

     S -> B ++: ReadRootHash(Store)
     B --> S --: {root: xyz, ...}

     S -> S: Verify(Listings)
     S --> C: RenderPage(Listings)
end

deactivate S

@enduml

“Retrieving the Listing” flowchart

Updating the Listing

To change the Listing we need to create Events. The Event will result in a new reduced state of the Listing, also known as the root hash. To keep Relays honest, this root hash is also saved on the block chain, to protect against omissions or other corruptions. See Store Configuration for more details.

Events have to have a known type. Currently we foresee:

  • Add new Item (which creates a new identity)

  • De-list an Item (marks the Identity invalid for future use but not non-existent)

  • Update metadata associated with an Item

  • Change stock count of an Item

  • Creating Carts and assigning Items to them

  • See schema.proto for a full list.

  • See Events mutate the listing for simplified visual illustration of the concept.

../_images/EventsMutateTheListing.png

Events mutate the listing

Note

We might have a 2nd class of Events, that are non-durable. Specifically, these might be cart-related events or other session data that will have to be kept in sync between relays but not mutate the listing, and thus on-chain data, immediately.

Selling Items

Now that we can add and remove Items from the Store, let’s discuss how they are actually sold with our system.

Warning

To protect from selling items twice, the Relays will help with book-keeping. To use an analogy from the physical world: the Relays will keep a virtual copy of the shopping cart, marking items as used once a cart is finalized or the process expires.

Building a Cart for a Customer

Note

This flow follows the assumption that we are in pop-up mode. Meaning, the Clerk will build the cart for the Customer. Changing this mode of operation to an e-commerce/self-checkout setting would not be that much different, though.

When a Clerk adds an Item to the Cart the Client creates an ChangeCart Event, which is sent to the Relay. The Relay checks the event and signals availability of Inventory back to the Store page. (As well as other Relays, in Phase 0.75 and onward.)

The “Building a Cart for a Customer” flowchart shows the interaction between the different systems.

@startuml
actor Customer as U
actor Clerk as C
boundary "Store Page" as S
database Relay as R
queue "Etherum Chain" as B

activate U
U -> C ++: "I want A, B and C, Please!"


== Prepare Cart ==

C -> S ++: Begin Cart

S -> B ++: getListingState(Store)

B --> S --: {root: d3adb33f, ...}

S --> S: check against local listing state

opt Item listing out of date

note right of C: See previous section on\nthe listing is updated

S --> C: busy:"updating listing"
S -> R ++: open sync connection(**KC**)

R --> S: EventPushReq{event1, ...}
...
R --> S --: eventN
R --> S: EventPushReq{event1, ...}

S --> S: apply and verify
end

S -> R ++: EventWrite{CreateCart{event_id:c44d}}
note right: in the future this alloc\n(and maybe even the cart as a sub-log)\nshould be synced with other relays.

R --> S --: err:null

S --> C --: signal:"cart ready"

C --> C: browse inventory for wanted items

C -> S ++: create and write events \nE = (addToCart(c44d,i) |  i of [A, B, C])

loop foreach e of E
S -> R ++: writeEvent(e)
R --> R: check item and cart extist, cart is not finalized, etc.

alt Add was valid
R --> S: Ok
S -> S: Update Cart UI
S --> C: Ok
else It wasn't
R --> S --: Err:Invalid
S -> S: Update Cart UI
S --> C: Show error
C -> U: "Sorry, something else?"
U --X U: reconsiders selection
end

deactivate S

end


@enduml

“Building a Cart for a Customer” flowchart

Warning

Item counts are checked against the total stock. Carts don’t lock up stuck until check-out has begun. This means that the stock count can change between adding an item to the cart and checking out. However, Clients can see when other carts use up stock (via ChangeCart) and will also receive ChangeStock Events when other carts completed checkout.

Completing the Purchase

Once the Cart is filled with the desired Items, the Clerk initiates the check-out via CommitCartRequest. This will collapse the Cart into a single update of the inventory and the Relay will yield a CartFinalized with the needed infromation, like the purchase address and the amount to pay. Once the Customer has paid their purchase, the ChangeStock Event is applied to the inventory by the Relay and on-chain data is updated.

The “Completing the Purchase” flowchart shows the interaction between the different systems.

@startuml
actor Customer as U
actor Clerk as C
boundary "Store Page" as S
database Relay as R
queue "Etherum Chain" as B

== Check out ==

C -> S ++: Click's "check out"

S -> R ++: Send CommitCartRequest{c44d}
R --> R: freeze card c444d (deny further changes)
R --> R: reduce cart into a single update and mark for next block

R -> R ++ #0d0: reduce cart into a single listing update

R --> R: sum total amount of cart\nTotalAmount = 9001

R --> R: Build ChangeStock Event: U = [A-1, B-1, ...]
R --> R: newRoot: 0x54135...
R --> R: recipteHash = 0xdeadbeef
note left of R: the recipteHash could be hash of the update U
R --> R --: {recipteHash, newRoot, ChangeStock, totalAmount}

R  -> R: Call getPaymentAddress(merchantAddr, totalAmount, recipteHash)\nPaymentAddress = b33f...

R -> R: Create Event CartFinalized{eventid=cf1, cart_id=c44d, totalAmount=9001, paymentAddress=b33f...}

R --> S: CommitCardResponse{cart_finalized_id=cf1}

R -> B++ #f58025: (hopefully not) poll the Chain for updates to b33f...

S -> S: generate QR code information\nCO={addr: b33f..., amount: 9001}


S -> U ++: showQRcode(CO)
U --> U: interacts with their wallet
U -> B --: sendFunds(b33f, 9001)

B --> R --: funds deposited
deactivate B

== Update Listing ==

R -> B ++: storeReg.updateRootHash{storeId, newRoot}

B --> R --: new block is X

R -> R ++  #a00: build Event ChangeStock{updates=U, cart_id=c44d}
R --> R --: sign event with relay wallet
R --> R: add event to store log

R --> S --: EventPush{ChangeStock{U, c44d}}

S --> S: emit H:"purchase complete"

S --> C--: checkout successful

C -> U --: hand over item(s)

@enduml

“Completing the Purchase” flowchart

Privacy Considerations

Warning

Now and next: There is a bunch of things in this current iteration we want to test, knowing that they won’t remain that way. There might be breaking changes in future versions of the protocol.

  • Listing data will be public - we want to make it possible to be indexed and aggregated in the future.
    • Inventory counts, too? This will give a clear transparent picture of how good or bad a store is doing.

  • Users that are running the store. This should be private/internal company data.
    • We need to think about how to protect this data from being leaked. Probably using seperate logs with stricter access control.

  • The merchantAddr will be where all purchases is are collected.
    • can someone backtrace all sales from that account address?

    • assuming yes, we will see the paymentAddr of individual sales and thus their account address, too?

  • What exactly hashes into the receiptHash?
    • Can listing changes be correlated to purchases?