{"record":{"id":"9ae98b08dcfd1951","repo":"hpyhacking/peatio","slug":"2007","errorCode":"2007","errorMessage":"The tonce #{tonce} is invalid, current timestamp is #{now}.","messagePattern":"The tonce #(.+?) is invalid, current timestamp is #(.+?)\\.","errorType":"exception","errorClass":"APIv2::InvalidTonceError","httpStatus":401,"severity":"error","filePath":"app/api/api_v2/auth/authenticator.rb","lineNumber":46,"sourceCode":"      def check_signature!\n        if @params[:signature] != Utils.hmac_signature(token.secret_key, payload)\n          Rails.logger.warn \"APIv2 auth failed: signature doesn't match. token: #{token.access_key} payload: #{payload}\"\n          raise IncorrectSignatureError, @params[:signature]\n        end\n      end\n\n      def check_tonce!\n        key = \"api_v2:tonce:#{token.access_key}:#{tonce}\"\n        if Utils.cache.read(key)\n          Rails.logger.warn \"APIv2 auth failed: used tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce}\"\n          raise TonceUsedError.new(token.access_key, tonce)\n        end\n        Utils.cache.write key, tonce, 61 # forget after 61 seconds\n\n        now = Time.now.to_i*1000\n        if tonce < now-30000 || tonce > now+30000 # within 30 seconds\n          Rails.logger.warn \"APIv2 auth failed: invalid tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce} current timestamp: #{now}\"\n          raise InvalidTonceError.new(tonce, now)\n        end\n      end\n\n      def tonce\n        @tonce ||= @params[:tonce].to_i\n      end\n\n      def payload\n        \"#{canonical_verb}|#{APIv2::Mount::PREFIX}#{canonical_uri}|#{canonical_query}\"\n      end\n\n      def canonical_verb\n        @request.request_method\n      end\n\n      def canonical_uri\n        @request.path_info\n      end","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/hpyhacking/peatio/blob/dab8641137c008928c835409342519bfef4eae7f/app/api/api_v2/auth/authenticator.rb#L28-L64","documentation":"Error code 2007 (InvalidTonceError) is raised by the window check in APIv2::Auth::Authenticator#check_tonce!: tonce must satisfy now-30000 <= tonce <= now+30000, where now is the server's Time.now in milliseconds. tonce is read with params[:tonce].to_i, so a seconds-precision timestamp (10 digits instead of 13) is always far below the window and rejected regardless of clock accuracy.","triggerScenarios":"Client clock skewed more than 30 seconds from the server; tonce sent in seconds instead of milliseconds; a signed request delayed or queued more than 30 seconds between tonce generation and delivery; container/VM clock drift.","commonSituations":"Laptop slept and resumed with a stale clock; Docker Desktop or VM host clock drift without NTP; offline or air-gapped CI runners; porting a client from an API that expects seconds.","solutions":["Send tonce as epoch milliseconds (Time.now.to_i * 1000) and transmit the request immediately after signing","Sync the machine clock (enable NTP/chrony; restart drifted Docker VMs)","If skew cannot be fixed, derive tonce from the server: GET /api/v2/timestamp returns server time in seconds — multiply by 1000 and add a counter for sub-millisecond bursts"],"exampleFix":"# before — seconds precision, local clock\nparams[:tonce] = Time.now.to_i\n\n# after — server-derived milliseconds\nserver_ms = JSON.parse(HTTP.get('https://host/api/v2/timestamp').body).to_i * 1000\nparams[:tonce] = server_ms + sequence_counter","handlingStrategy":"validation","validationCode":"# Before signing, confirm local time is inside the server's +/-30s window\nserver_now = JSON.parse(HTTP.get('https://host/api/v2/timestamp').body) * 1000\nskew = Time.now.to_i * 1000 - server_now\nraise \"clock skew #{skew}ms exceeds 30s — sync NTP or use server time\" if skew.abs > 25_000\nparams[:tonce] = server_now","typeGuard":null,"tryCatchPattern":"The 2007 message includes the server's current millisecond timestamp ('current timestamp is <now>'); on catch, compute offset = server_now - tonce_sent, correct the client's tonce base, then resend with a fresh tonce and signature once, instead of blind-retrying.","preventionTips":["Run NTP/chrony on hosts and CI runners that sign requests","Use milliseconds, never seconds, for tonce","Fire the request immediately after signing; never sign requests ahead of time into a queue","Prefer server time (GET /api/v2/timestamp) as the tonce source when client clocks are unreliable"],"tags":["ruby","api-auth","tonce","clock-skew","timestamp"],"backgroundTag":"clock-skew-timestamp-rejected","analyzedSha":"dab8641137c008928c835409342519bfef4eae7f","analyzedAt":"2026-08-23T09:59:18.005Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}