@@ -4,26 +4,32 @@ import * as blake2 from 'blake2';
44import bs58check from 'bs58check' ;
55import Client from 'mina-signer' ;
66import { submitterAllowed } from './submitterValidator' ;
7+ import { CallableRequest , onCall } from 'firebase-functions/v2/https' ;
8+ import { getFirestore , FieldValue } from 'firebase-admin/firestore' ;
79
810interface SignatureJson {
911 field : string ;
1012 scalar : string ;
1113}
1214
1315interface HeartbeatData {
14- publicKey : string ;
15- data : string ;
16+ version : number ;
17+ payload : string ;
18+ submitter : string ;
1619 signature : SignatureJson ;
1720}
1821
1922const minaClient = new Client ( { network : 'testnet' } ) ;
2023
2124admin . initializeApp ( ) ;
2225
26+ // Rate limit duration between heartbeats from the same submitter (15 seconds)
27+ const HEARTBEAT_RATE_LIMIT_MS = 15000 ;
28+
2329function validateSignature (
2430 data : string ,
2531 signature : SignatureJson ,
26- publicKeyBase58 : string
32+ publicKeyBase58 : string ,
2733) : boolean {
2834 try {
2935 const h = blake2 . createHash ( 'blake2b' , { digestLength : 32 } ) ;
@@ -62,49 +68,55 @@ function validateSignature(
6268 }
6369}
6470
65- export const handleValidationAndStore = functions
66- . region ( 'us-central1' )
67- . https . onCall ( async ( data : HeartbeatData , context : functions . https . CallableContext ) => {
68- console . log ( 'Received data:' , data ) ;
69- const { publicKey, data : inputData , signature } = data ;
71+ export const handleValidationAndStore = onCall (
72+ { region : 'us-central1' , enforceAppCheck : false } ,
73+ async ( request : CallableRequest < HeartbeatData > ) => {
74+ console . log ( 'Received data:' , request . data ) ;
75+ const data = request . data ;
76+ const { submitter, payload, signature } = data ;
7077
71- if ( ! submitterAllowed ( publicKey ) ) {
78+ if ( ! submitterAllowed ( submitter ) ) {
7279 throw new functions . https . HttpsError (
7380 'permission-denied' ,
74- 'Public key not authorized'
81+ 'Public key not authorized' ,
7582 ) ;
7683 }
7784
78- const rateLimitRef = admin . firestore ( ) . collection ( 'publicKeyRateLimits' ) . doc ( publicKey ) ;
85+ const db = getFirestore ( ) ;
7986
8087 try {
81- await admin . firestore ( ) . runTransaction ( async ( transaction ) => {
88+ if ( ! validateSignature ( payload , signature , submitter ) ) {
89+ throw new functions . https . HttpsError (
90+ 'unauthenticated' ,
91+ 'Signature validation failed' ,
92+ ) ;
93+ }
94+
95+ const rateLimitRef = db . collection ( 'publicKeyRateLimits' ) . doc ( submitter ) ;
96+ const newHeartbeatRef = db . collection ( 'heartbeats' ) . doc ( ) ;
97+
98+ await db . runTransaction ( async ( transaction ) => {
8299 const doc = await transaction . get ( rateLimitRef ) ;
83100 const now = Date . now ( ) ;
84- const cutoff = now - 15 * 1000 ;
101+ const cutoff = now - HEARTBEAT_RATE_LIMIT_MS ;
85102
86103 if ( doc . exists ) {
87- const lastCall = doc . data ( ) ?. lastCall ;
104+ const lastCall = doc . data ( ) ?. [ ' lastCall' ] ;
88105 if ( lastCall > cutoff ) {
89106 throw new functions . https . HttpsError (
90107 'resource-exhausted' ,
91- 'Rate limit exceeded for this public key'
108+ 'Rate limit exceeded for this public key' ,
92109 ) ;
93110 }
94111 }
95112
96- transaction . set ( rateLimitRef , { lastCall : now } , { merge : true } ) ;
113+ transaction . set ( rateLimitRef , { lastCall : FieldValue . serverTimestamp ( ) } , { merge : true } ) ;
114+ transaction . create ( newHeartbeatRef , {
115+ ...data ,
116+ createTime : FieldValue . serverTimestamp ( ) ,
117+ } ) ;
97118 } ) ;
98119
99- if ( ! validateSignature ( inputData , signature , publicKey ) ) {
100- throw new functions . https . HttpsError (
101- 'unauthenticated' ,
102- 'Signature validation failed'
103- ) ;
104- }
105-
106- await admin . firestore ( ) . collection ( 'heartbeat' ) . add ( data ) ;
107-
108120 return { message : 'Data validated and stored successfully' } ;
109121 } catch ( error ) {
110122 console . error ( 'Error during data validation and storage:' , error ) ;
@@ -113,9 +125,10 @@ export const handleValidationAndStore = functions
113125 }
114126 throw new functions . https . HttpsError (
115127 'internal' ,
116- 'An error occurred during validation or storage'
128+ 'An error occurred during validation or storage' ,
117129 ) ;
118130 }
119- } ) ;
131+ } ,
132+ ) ;
120133
121134export { validateSignature } ;
0 commit comments