{"record":{"id":"58151b42d11c624f","repo":"hpyhacking/peatio","slug":"2006","errorCode":"2006","errorMessage":"The tonce #{tonce} has already been used by access key #{access_key}.","messagePattern":"The tonce #(.+?) has already been used by access key #(.+?)\\.","errorType":"exception","errorClass":"APIv2::TonceUsedError","httpStatus":401,"severity":"error","filePath":"app/api/api_v2/auth/authenticator.rb","lineNumber":39,"sourceCode":"      def check_token!\n        raise InvalidAccessKeyError, @params[:access_key] unless token\n        raise DisabledAccessKeyError, @params[:access_key] if token.member.api_disabled\n        raise ExpiredAccessKeyError, @params[:access_key] if token.expired?\n        raise OutOfScopeError unless token.in_scopes?(route_scopes)\n      end\n\n      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","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/hpyhacking/peatio/blob/dab8641137c008928c835409342519bfef4eae7f/app/api/api_v2/auth/authenticator.rb#L21-L57","documentation":"Error code 2006 (TonceUsedError) is raised by APIv2::Auth::Authenticator#check_tonce! when Utils.cache (a Rack::Attack cache wrapper, so a shared store when Redis-backed) already holds the key \"api_v2:tonce:<access_key>:<tonce>\". Every tonce that passes this check is written to the cache for 61 seconds, so the same access key cannot reuse a tonce within that window — this is replay protection. Because check_tonce! runs before signature verification, even a request that failed with 2005 (bad signature) has already burned its tonce.","triggerScenarios":"Resending a request verbatim after a timeout or a 2005/2007 auth failure; an HTTP client or proxy retry layer re-posting the identical signed body; two threads or workers sharing one tonce value; a tonce computed once at client startup and reused for every request in a loop.","commonSituations":"Idempotent-retry wrappers (faraday/axios retry interceptors, curl --retry in scripts) that replay the signed body unchanged; batch scripts pinning a constant tonce; a first attempt rejected for bad signature and a naive verbatim retry that then hits 2006 instead.","solutions":["Generate a fresh tonce per attempt: current epoch milliseconds plus a monotonic counter if you can burst more than one request per millisecond, and re-sign the payload with it","Disable or reconfigure HTTP-client auto-retry so it can never replay the signed body unchanged","When several processes sign concurrently, derive tonce from clock plus a per-process counter so two signers never emit the same value"],"exampleFix":"# before — tonce fixed once, replayed by a retry helper\ntonce = Time.now.to_i * 1000\nretryable.post('/api/v2/orders', params.merge(tonce: tonce, signature: sig))\n\n# after — unique tonce on every attempt, re-sign before each resend\ndef next_tonce\n  @seq = ((@seq || 0) + 1) % 1_000\n  Time.now.to_i * 1_000_000 + @seq  # unique even within the same millisecond\nend\n# on ANY failure (2005/2006/2007, timeouts): rebuild params with next_tonce and re-sign","handlingStrategy":"retry","validationCode":"# Track the last tonce per access key; never send a duplicate\ndef next_tonce(access_key)\n  @last ||= {}\n  t = Time.now.to_i * 1000\n  t += 1 while t <= @last[access_key]  # guarantee strictly increasing\n  @last[access_key] = t\n  t\nend","typeGuard":null,"tryCatchPattern":"On response code 2006, first verify the earlier attempt truly failed (query order/deposit state — the first request may have executed on the server); only then regenerate the tonce, re-sign the payload, and resend exactly once.","preventionTips":["Never reuse a request body containing tonce/signature: retries rebuild both from scratch","Disable HTTP-client auto-retry or configure it to re-invoke the signer","Burst-heavy clients add a monotonic counter to millisecond timestamps","Centralize tonce generation in one class instead of sprinkling Time.now calls across call sites"],"tags":["ruby","api-auth","tonce","nonce","replay-protection","cache"],"backgroundTag":"nonce-already-used","analyzedSha":"dab8641137c008928c835409342519bfef4eae7f","analyzedAt":"2026-08-23T09:59:18.005Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}