- Overview
- What are AATs?
- Generating an AAT
- Retrieve the latest session for your application
- What are relay proofs?
- Generating a relay to the network
- Final Steps
To truly understand why gateway operators and the Nodies Gateway Stack are important, let's review what steps application developers have to perform in order to send a single request to Pocket Network without gateways.
The chronological steps assuming the application is staked are:
- Generate an Application Authentication Token (AAT)
- Send a request to a Pocket full node for the latest nodes in a session
- Construct and sign a Relay Proof and submit it to one of the nodes in a session
- Receive a response from a node
- Determine if the response is legit or valid
- Proxy it back to your Web Application
The AAT is an auth token that allows application clients to access the network without the need to expose their private keys.
Note: AATs are non-revocable and do not have a time expiration date. The only way to revoke a token is to unstake the entire application
AAT's Data Structure
type AAT struct {
Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version"`
ApplicationPublicKey string `protobuf:"bytes,2,opt,name=applicationPublicKey,proto3" json:"app_pub_key"`
ClientPublicKey string `protobuf:"bytes,3,opt,name=clientPublicKey,proto3" json:"client_pub_key"`
ApplicationSignature string `protobuf:"bytes,4,opt,name=applicationSignature,proto3" json:"signature"`
}
JSON AAT Example
{
"version": "0.0.1",
"app_pub_key": "eb0cf2a891382677f03c1b080ec270c693dda7a4c3ee4bcac259ad47c5fe0743",
"client_pub_key": "eb0cf2a891382677f03c1b080ec270c693dda7a4c3ee4bcac259ad47c5fe0743",
"signature": "5309f66a22ace63e8b4f94220151feabad11d4f3c22f50f6e395c72f1df96111da9bb25eceb11361d7e7074e7105d57dd2ec1d85cf962460608ef4bc5d35a80a"
}
- The AAT specification can be found here, but in simple terms:
- JSON Encode AAT with an empty string signature field:
- SHA3_256 the JSON bytes (bytes of the stringified JSON Object)
- Sign with ED25519 ECDSA
- HexEncode the result bytes into a string
- Replace the empty AAT.signature field with the hex-encoded result.
An example code implementation of this can be found in both Javascript and Golang
An application is supported by 24 randomly chosen node runners for a short duration called a session
, which currently lasts 4 blocks.
Once that session is over, the application gets a new set of node runners. To find out who the latest node runners are, the application connects to a Pocket node and sends it a request. This process is called dispatching.
Request (POST): {pocket_host}/v1/client/dispatch
with the following payload:
{
"app_public_key": "514810e9139c5571905c642564b18cfb67899af2da05e638031075033da091a5",
"chain": "0074"
}
Response:
{
"block_height": 108183,
"session": {
"header": {
"app_public_key": "514810e9139c5571905c642564b18cfb67899af2da05e638031075033da091a5",
"chain": "0074",
"session_height": 108181
},
"key": "EKxfv3DhF8u7gn1dhZxjFPQFhE+FTGhjUtLCsnq6V4g=",
"nodes": [
{
"address": "cd019c3b62cfb8cb9fd9863634fd42f2caef8984",
"chains": [
"0021",
"0027",
"0052",
"0003",
"0006",
"0053",
"0004",
"0065",
"0070",
"0054",
"0048",
"0058",
"000F",
"0074",
"0009"
],
"jailed": false,
"output_address": "344e7bd9fc60a7f91f91b44219a5e7ef99af9810",
"public_key": "fd332ff15904c5b6d68f42aa5f05c66e9f2d9ba8267014e80112a1a39105f4e8",
"service_url": "https://6286.n.poktstaking.com:443",
"status": 2,
"tokens": "60010000000",
"unstaking_time": "0001-01-01T00:00:00Z"
}
Note: Given that retrieving a session requires a full node, this means staked applications will need to source full nodes or run one themselves! As well, this acts as a failure point for app/gateway operators if they only rely on one full node for dispatching. Without a session, application developers cannot send a request. Thankfully thanks to pruning efforts and more full nodes entering the networks, this should become a lower risk.
At its core, a relay proof is like a digital receipt proving that an application sent a request to a node runner. Here's how it works:
- Generation and Validation: When an application makes a request, it creates and signs a 'relay proof'. This is like a digital signature, ensuring the request is genuine and hasn't been tampered with.
- Verification by Servicer: These servicers check the relay proof to make sure it's from a legitimate application in the network.
- Storing the Proof: Once verified, node runners store this proof in a data structure called a 'Merle Sum Index (MSI) Tree'.
- Processing the Request: The node runner then processes the request and sends the information back to the application.
- Claim and Proof Lifecycle: In the process of getting paid for their work, node runners go through a two-step 'claim and proof' cycle. First, they submit the 'root' of the MSI Tree as part of a claim transaction, indicating they have served several requests. Then, to provide evidence of their work, they must submit a randomly selected index along with Merkle proof from the branch to root, ensuring fairness and verification. This allows the network to trust that the node runners aren't just selecting an easy-to-prove transaction but deterministically chosen by the network in a secure way. Ultimately, this allows for a compute and space-efficient blockchain as validators of the network do not have to store nor verify every single request served.
Now that we have access to a set of node runners (and assuming all node runners are actually operational), we still need to send the JSON-RPC request in a data structure that node runners will understand and accept. Unfortunately, node runners will not accept a simple HTTP JSON-RPC request as you would expect with other node providers, so we must construct a relay along with a relay proof.
POST request to {pocket_host}/v1/client/relay
with the following payload
{
"payload": "relay_payload",
"meta": "relay_meta",
"proof": "relay_proof"
}
type Payload struct {
Data string `json:"data"` // the actual data string for the external chain
Method string `json:"method"` // the http CRUD method
Path string `json:"path"` // the REST Path
Headers map[string]string `json:"headers,omitempty"` // http headers
}
{
"data": "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"0x050ea4ab4183E41129B7D72A492DaBf52B27EdB5\",\"latest\"],\"id\":67}",
"method": "POST",
"path": "",
"headers": null
}
Relay metadata is simply when the request is made based on POKT's block time. This can be simplified to the session block height. However, if possible, the application developer should return the block height from /v1/query/height
type RelayMeta struct {
BlockHeight int64 `json:"block_height"` // the block height when the request is made
}
{
"block_height": 100
}
The generation of the relay proof is the most complex and crucial piece to ensuring the application's request is recognized and accepted by the node runners.
type RelayProof struct {
RequestHash string `protobuf:"bytes,1,opt,name=requestHash,proto3" json:"request_hash"`
Entropy int64 `protobuf:"varint,2,opt,name=entropy,proto3" json:"entropy"`
SessionBlockHeight int64 `protobuf:"varint,3,opt,name=sessionBlockHeight,proto3" json:"session_block_height"`
ServicerPubKey string `protobuf:"bytes,4,opt,name=servicerPubKey,proto3" json:"servicer_pub_key"`
Blockchain string `protobuf:"bytes,5,opt,name=blockchain,proto3" json:"blockchain"`
Token AAT `protobuf:"bytes,6,opt,name=token,proto3" json:"aat"`
Signature string `protobuf:"bytes,7,opt,name=signature,proto3" json:"signature"`
}
-
Entropy Generate a random integer from [0, INT64]
-
SessionBlockHeight
,Blockchain
,ServicerPubKey
,Token
are retrievable from the above steps. -
RequestHash
is generated by with the following pseudo-code// requestHash { "payload": relay_payload, "meta": relay_meta } requestHashBytes := json.Marshal(requestHash) (bytes of the stringified JSON Object) SHA3_256(requestHashBytes) HexEncode to string
-
Once the relay object is generated, construct an ordered version of the relay object to hash and sign with the application private key for a signature using the following order: ng the following order:
// relayProof { "entropy": "1234567890123456", "session_block_height": 108181, "servicer_pub_key": "a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890", "blockchain": "0074", "signature": "", "token": "SHA3_256-aat-without-signature", "request_hash": "sha-256-request-hash" } // json encode relay proof relayProofJsonBytes := json.Marshal(relayProof) hashedRelayProof = SHA3_256(relayProofJsonBytes) SIGN(hashedRelayProof, appPrivateKey) -> c1d2e3f4a5b67890c1d2e3f4a5b67890c1d2e3f4a5b67890c1d2e3f4a5b67890
-
Use the generated signature to fill out the missing Signature field
NOTE: Ordering of the JSON object matters because the values are hashed. If the ordering changes, so will the hash.
With all the fields now generated, the valid relay proof can be constructed as below:
-
Relay Proof with Signature
{ "request_hash": "b1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "entropy": 1234567890123456, "session_block_height": 108181, "servicer_pub_key": "a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890", "blockchain": "0074", "aat": { ... // includes signature }, "signature": "c1d2e3f4a5b67890c1d2e3f4a5b67890c1d2e3f4a5b67890c1d2e3f4a5b67890" }
-
Send to /v1/client/relay
{ "payload": "relay_payload", "meta": "relay_meta", "proof": "relay_proof_with_signature" }
If all goes well, the application should receive a response from the node runner!
After delving into the complexities of selecting a reliable source of dispatchers to retrieve a session, considering the network does not offer QoS assurances, and grasping the intricacies of sending requests to node runners, it becomes evident how crucial it is for software to abstract away the protocol and foster true developer adoption. This highlights the importance of the Gateway Operators and ultimately the Gateway server vision.