hpyhacking/peatio · error · APIv2::InvalidTonceError

2007

2007

Error message

The tonce #{tonce} is invalid, current timestamp is #{now}.

What it means

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.

Source

Thrown at app/api/api_v2/auth/authenticator.rb:46

      def check_signature!
        if @params[:signature] != Utils.hmac_signature(token.secret_key, payload)
          Rails.logger.warn "APIv2 auth failed: signature doesn't match. token: #{token.access_key} payload: #{payload}"
          raise IncorrectSignatureError, @params[:signature]
        end
      end

      def check_tonce!
        key = "api_v2:tonce:#{token.access_key}:#{tonce}"
        if Utils.cache.read(key)
          Rails.logger.warn "APIv2 auth failed: used tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce}"
          raise TonceUsedError.new(token.access_key, tonce)
        end
        Utils.cache.write key, tonce, 61 # forget after 61 seconds

        now = Time.now.to_i*1000
        if tonce < now-30000 || tonce > now+30000 # within 30 seconds
          Rails.logger.warn "APIv2 auth failed: invalid tonce. token: #{token.access_key} payload: #{payload} tonce: #{tonce} current timestamp: #{now}"
          raise InvalidTonceError.new(tonce, now)
        end
      end

      def tonce
        @tonce ||= @params[:tonce].to_i
      end

      def payload
        "#{canonical_verb}|#{APIv2::Mount::PREFIX}#{canonical_uri}|#{canonical_query}"
      end

      def canonical_verb
        @request.request_method
      end

      def canonical_uri
        @request.path_info
      end

View on GitHub (pinned to dab8641137)

Solutions

  1. Send tonce as epoch milliseconds (Time.now.to_i * 1000) and transmit the request immediately after signing
  2. Sync the machine clock (enable NTP/chrony; restart drifted Docker VMs)
  3. 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

Example fix

# before — seconds precision, local clock
params[:tonce] = Time.now.to_i

# after — server-derived milliseconds
server_ms = JSON.parse(HTTP.get('https://host/api/v2/timestamp').body).to_i * 1000
params[:tonce] = server_ms + sequence_counter
Defensive patterns

Strategy: validation

Validate before calling

# Before signing, confirm local time is inside the server's +/-30s window
server_now = JSON.parse(HTTP.get('https://host/api/v2/timestamp').body) * 1000
skew = Time.now.to_i * 1000 - server_now
raise "clock skew #{skew}ms exceeds 30s — sync NTP or use server time" if skew.abs > 25_000
params[:tonce] = server_now

Try / catch

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.

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of hpyhacking/peatio@dab8641137 (2026-08-23). Data as JSON: /api/errors/9ae98b08dcfd1951. Report an issue: GitHub.