From ea406387dd9f10ce0a5e3bbad3b544c4a71651b1 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 26 Apr 2018 17:17:00 +0300 Subject: [PATCH] Add TxTracker protocol; introduce [un]track-tx! fns for generic tx tracking --- resources/sql/queries.sql | 82 ++++---- src/clj/commiteth/bounties.clj | 10 +- src/clj/commiteth/db/bounties.clj | 21 -- src/clj/commiteth/db/issues.clj | 31 ++- src/clj/commiteth/eth/core.clj | 222 ++++++++++++++-------- src/clj/commiteth/eth/multisig_wallet.clj | 11 +- src/clj/commiteth/scheduler.clj | 49 +++-- 7 files changed, 229 insertions(+), 197 deletions(-) diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index d8efbd2..3028c73 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -150,31 +150,6 @@ SET transaction_hash = :transaction_hash, WHERE issue_id = :issue_id; --- TODO: this is terrible --- :name update-contract-address : nonce (:nonce @nonce-being-mined))) - (:nonce (reset! nonce-being-mined (TxNonce. nil nonce nil nil))) - (throw (Exception. (str "Attempting to re-use old nonce" nonce)))))) + (.. (.ethGetTransactionCount @web3j-obj + (env :eth-account) + DefaultBlockParameterName/PENDING) + sendAsync + get + getTransactionCount)) + + +(defprotocol TxTracker + (try-reserve-nonce [this]) + (drop-nonce [this nonce]) + (track-tx [this tx-info]) + (untrack-tx [this tx-info]) + (prune-txs [this unmined-txs]) + ) + +(defrecord SequentialTxTracker [current-tx] + TxTracker + (try-reserve-nonce [this] + (let [nonce (get-nonce)] + (if (or (nil? @current-tx) + (> nonce (:nonce @current-tx))) + (:nonce (reset! current-tx {:nonce nonce})) + (throw (Exception. (str "Attempting to re-use old nonce" nonce)))))) + (drop-nonce [this nonce] + (reset! current-tx nil)) + (track-tx [this tx-info] + (reset! current-tx tx-info)) + (untrack-tx [this tx-info] + (reset! current-tx nil)) + (prune-txs [this unmined-txs] + (when (or ((set (map :tx-hash unmined-txs)) (:tx-hash @current-tx)) + (and (:timestamp @current-tx) + (t/before? (:timestamp @current-tx) (t/minus (t/now) (t/minutes 5))))) + (log/errorf "Current nonce unmined for 5 minutes, force reset. Tx hash: %s, type: %s" + (:tx-hash @current-tx) (:type @current-tx)) + (reset! current-tx nil))) + + ) +(def tx-tracker (SequentialTxTracker. (atom nil))) + +(defn track-tx! [{:keys [issue-id tx-hash type] + :as tx-info}] + (track-tx tx-tracker tx-info) + (issues/save-tx-info! issue-id tx-hash type)) + +(defn untrack-tx! [{:keys [issue-id result type] + :as tx-info}] + (untrack-tx tx-tracker tx-info) + (issues/save-tx-result! issue-id result type)) + +(defn prune-txs! [unmined-txs] + (doseq [{issue-id :issue_id + tx-hash :tx_hash + type :type} unmined-txs] + (log/infof "issue %s: resetting tx operation: %s for hash: %s" issue-id type tx-hash) + (issues/save-tx-info! issue-id nil type)) + (prune-txs tx-tracker unmined-txs)) + + (defn get-signed-tx [gas-price gas-limit to data nonce] "Create a sign a raw transaction. - 'From' argument is not needed as it's already - encoded in credentials. - See https://web3j.readthedocs.io/en/latest/transactions.html#offline-transaction-signing" - (let [tx (RawTransaction/createTransaction - nonce - gas-price - gas-limit - to - data) - signed (TransactionEncoder/signMessage tx (creds)) - hex-string (Numeric/toHexString signed)] - (log/infof "Signing TX: nonce: %s, gas-price: %s, gas-limit: %s, data: %s" - nonce gas-price gas-limit data) - hex-string)) + 'From' argument is not needed as it's already + encoded in credentials. + See https://web3j.readthedocs.io/en/latest/transactions.html#offline-transaction-signing" + (log/infof "Signing TX: nonce: %s, gas-price: %s, gas-limit: %s, data: %s" + nonce gas-price gas-limit data) + (-> (RawTransaction/createTransaction nonce gas-price gas-limit to data) + (TransactionEncoder/signMessage (creds)) + (Numeric/toHexString))) (defn eth-gasstation-gas-price @@ -135,7 +177,9 @@ (defn eth-rpc [{:keys [method params internal-tx-id]}] {:pre [(string? method) (some? params)]} - (let [request-id (swap! req-id-tracker inc) + (let [[type-kw issue-id] internal-tx-id + tx-id-str (str type-kw "-" issue-id) + request-id (swap! req-id-tracker inc) body {:jsonrpc "2.0" :method method :params params @@ -145,10 +189,10 @@ response @(post (eth-rpc-url) options) result (safe-read-str (:body response))] (when internal-tx-id - (log/infof "%s: eth-rpc %s" internal-tx-id method)) - (log/debugf "%s: eth-rpc req(%s) body: %s" internal-tx-id request-id body) - (if internal-tx-id - (log/infof "%s: eth-rpc req(%s) result: %s" internal-tx-id request-id result) + (log/infof "%s: eth-rpc %s" tx-id-str method)) + (log/debugf "%s: eth-rpc req(%s) body: %s" tx-id-str request-id body) + (if tx-id-str + (log/infof "%s: eth-rpc req(%s) result: %s" tx-id-str request-id result) (log/debugf "no-tx-id: eth-rpc req(%s) result: %s" request-id result)) (cond ;; Ignore any responses that have mismatching request ID @@ -159,7 +203,7 @@ (:error result) (throw (ex-info (format "%s: Error submitting transaction via eth-rpc %s" - (or internal-tx-id "(no-tx-id)") (:error result)) + (or tx-id-str "(no-tx-id)") (:error result)) (:error result))) :else @@ -243,13 +287,8 @@ (defn get-transaction-receipt [hash] - (when-let [receipt (eth-rpc {:method "eth_getTransactionReceipt" - :params [hash]})] - (if (= hash (:tx-hash @nonce-being-mined)) - (reset! nonce-being-mined nil) - ;; This can happen in case of payout receipts - (log/infof "Obtained receipt for tx not currently being mined: " hash)) - receipt)) + (eth-rpc {:method "eth_getTransactionReceipt" + :params [hash]})) (defn format-call-params [method-id & params] @@ -262,47 +301,68 @@ (eth-rpc {:method "eth_call" :params [{:to contract :data data} "latest"]}))) -(defn execute - [{:keys [from contract method-id gas-limit params internal-tx-id type]}] - {:pre [(string? method-id)]} +(defn construct-params + [{:keys [from contract method-id params gas-price]}] (let [data (apply format-call-params method-id params) - gas-price (gas-price) - value (format "0x%x" 0) - params (cond-> {:data data - :from from - :value value} - gas-price - (merge {:gasPrice (integer->hex gas-price)}) - contract - (merge {:to contract})) + value (format "0x%x" 0)] + (cond-> {:data data + :from from + :value value} + gas-price + (merge {:gasPrice (integer->hex gas-price)}) + contract + (merge {:to contract})))) + +(defmulti execute (fn [arg] (offline-signing?))) + + +(defmethod execute true + [{:keys [from contract method-id gas-limit params internal-tx-id] + :as args}] + {:pre [(string? method-id)]} + (let [[type-kw issue-id] internal-tx-id + nonce (try-reserve-nonce tx-tracker) + gas-price (gas-price) + params (construct-params (assoc args :gas-price gas-price)) + gas (or gas-limit (estimate-gas from contract (:value params) params)) + params (get-signed-tx (biginteger gas-price) + (hex->big-integer gas) + contract + (:data params) + nonce) + tx-hash (try + (eth-rpc + {:method "eth_sendRawTransaction" + :params [params] + :internal-tx-id internal-tx-id}) + (catch Throwable ex + (drop-nonce tx-tracker nonce) + (throw ex)))] + {:tx-hash tx-hash + :issue-id issue-id + :type type-kw + :nonce nonce + :timestamp (t/now)})) + +(defmethod execute false + [{:keys [from contract method-id gas-limit params internal-tx-id] + :as args}] + {:pre [(string? method-id)]} + (let [[type-kw issue-id] internal-tx-id + gas-price (gas-price) + params (construct-params (assoc args :gas-price gas-price)) + gas (or gas-limit (estimate-gas from contract (:value params) params)) + params (assoc params :gas gas) + tx-hash (eth-rpc + {:method "personal_sendTransaction" + :params [params (eth-password)] + :internal-tx-id internal-tx-id})] + {:tx-hash tx-hash + :type type-kw + :timestamp (t/now) + :issue-id issue-id} + )) - gas (if gas-limit gas-limit - (estimate-gas from contract value params)) - nonce (when (offline-signing?) (get-nonce)) - params (if (offline-signing?) - (get-signed-tx (biginteger gas-price) - (hex->big-integer gas) - contract - data - nonce) - (assoc params :gas gas))] - (if (offline-signing?) - (try - (let [tx-hash (eth-rpc - {:method "eth_sendRawTransaction" - :params [params] - :internal-tx-id internal-tx-id})] - (:tx-hash (swap! nonce-being-mined - assoc :tx-hash tx-hash - :type type - :timestamp (t/now)))) - (catch Throwable ex - (reset! nonce-being-mined nil) - (throw ex))) - (eth-rpc - {:method "personal_sendTransaction" - :params [params (eth-password)] - :internal-tx-id internal-tx-id})))) (defn hex-ch->num [ch] diff --git a/src/clj/commiteth/eth/multisig_wallet.clj b/src/clj/commiteth/eth/multisig_wallet.clj index fd1bbef..09c4d8f 100644 --- a/src/clj/commiteth/eth/multisig_wallet.clj +++ b/src/clj/commiteth/eth/multisig_wallet.clj @@ -41,9 +41,8 @@ `internal-tx-id` is used to identify what issue this multisig is deployed for and manage nonces at a later point in time." [{:keys [owner internal-tx-id]}] - {:pre [(string? owner) (string? internal-tx-id)]} + {:pre [(string? owner) (vector? internal-tx-id)]} (eth/execute {:internal-tx-id internal-tx-id - :type "deploy" :from (eth/eth-account) :contract (factory-contract-addr) :method-id (:create method-ids) @@ -88,13 +87,12 @@ (defn send-all [{:keys [contract payout-address internal-tx-id]}] - {:pre [(string? contract) (string? payout-address) (string? internal-tx-id)]} + {:pre [(string? contract) (string? payout-address) (vector? internal-tx-id)]} (log/debug "multisig/send-all " contract payout-address internal-tx-id) (let [params (eth/format-call-params (:withdraw-everything method-ids) payout-address)] (eth/execute {:internal-tx-id internal-tx-id - :type "execute" :from (eth/eth-account) :contract contract :method-id (:submit-transaction method-ids) @@ -108,12 +106,11 @@ (:address token-details))) (defn watch-token - [bounty-addr token] + [{:keys [bounty-addr token internal-tx-id]}] (log/debug "multisig/watch-token" bounty-addr token) (let [token-address (get-token-address token)] (assert token-address) - (eth/execute {:internal-tx-id (str "watch-token-" (System/currentTimeMillis) "-" bounty-addr) - :type "watch" + (eth/execute {:internal-tx-id internal-tx-id :from (eth/eth-account) :contract bounty-addr :method-id (:watch method-ids) diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index aecfc92..be5b814 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -52,11 +52,14 @@ (when-let [receipt (eth/get-transaction-receipt transaction-hash)] (log/infof "issue %s: update-issue-contract-address: tx receipt: %s" issue-id receipt) (if-let [contract-address (multisig/find-created-multisig-address receipt)] - (let [issue (issues/update-contract-address issue-id contract-address) + (let [_ (eth/untrack-tx! {:issue-id issue-id + :tx-hash transaction-hash + :result contract-address + :type :deploy}) {owner :owner repo :repo comment-id :comment_id - issue-number :issue_number} issue + issue-number :issue_number} (issues/get-issue-by-id issue-id) balance-eth-str (eth/get-balance-eth contract-address 6) balance-eth (read-string balance-eth-str)] (log/infof "issue %s: Updating comment image" issue-id) @@ -112,7 +115,7 @@ issue-number :issue_number balance-eth :balance_eth tokens :tokens - winner-login :winner_login} (db-bounties/pending-bounties)] + winner-login :winner_login} (db-bounties/pending-bounties)] (try (let [value (eth/get-balance-hex contract-address)] (if (empty? payout-address) @@ -126,11 +129,11 @@ tokens winner-login true)) - (let [execute-hash (multisig/send-all {:contract contract-address - :payout-address payout-address - :internal-tx-id (str "payout-github-issue-" issue-id)})] - (log/infof "issue %s: Payout self-signed, called sign-all(%s) tx: %s" issue-id contract-address payout-address execute-hash) - (db-bounties/update-execute-hash issue-id execute-hash) + (let [tx-info (multisig/send-all {:contract contract-address + :payout-address payout-address + :internal-tx-id [:execute issue-id]})] + (log/infof "issue %s: Payout self-signed, called sign-all(%s) tx: %s" issue-id contract-address payout-address (:tx-hash tx-info)) + (eth/track-tx! tx-info) (db-bounties/update-winner-login issue-id winner-login) (github/update-merged-issue-comment owner repo @@ -157,7 +160,10 @@ (log/infof "issue %s: execution receipt for issue " issue-id receipt) (when-let [confirm-hash (multisig/find-confirmation-tx-id receipt)] (log/infof "issue %s: confirm hash:" issue-id confirm-hash) - (db-bounties/update-confirm-hash issue-id confirm-hash))) + (eth/untrack-tx! {:issue-id issue-id + :tx-hash execute-hash + :result confirm-hash + :type :execute}))) (catch Throwable ex (log/errorf ex "issue %s: update-confirm-hash exception:" issue-id))))) (log/info "Exit update-confirm-hash")) @@ -172,7 +178,10 @@ (log/infof "issue %s: pending watch call %s" issue-id watch-hash) (try (when-let [receipt (eth/get-transaction-receipt watch-hash)] - (db-bounties/update-watch-hash issue-id nil)) + (eth/untrack-tx! {:issue-id issue-id + :tx-hash watch-hash + :result nil + :type :watch})) (catch Throwable ex (log/errorf ex "issue %s: update-watch-hash exception:" issue-id)))))) @@ -260,8 +269,10 @@ (when (and (nil? watch-hash) (not= balance internal-balance)) (log/infof "bounty %s: balances not in sync, calling watch" bounty-addr) - (let [hash (multisig/watch-token bounty-addr tla)] - (db-bounties/update-watch-hash issue-id hash))))))) + (let [tx-info (multisig/watch-token {:bounty-addr bounty-addr + :token tla + :internal-tx-id [:watch issue-id]})] + (eth/track-tx! tx-info))))))) (catch Throwable ex (log/error ex "bounty %s: update-bounty-token-balances exception" bounty-addr)))) (log/info "Exit update-bounty-token-balances")) @@ -376,19 +387,7 @@ as we are executing txs sequentially" [] (log/info "In check-tx-receipts") - (doseq [{tx-hash :tx_hash - type :type - issue-id :issue_id} (db-bounties/unmined-tx-hashes)] - (log/infof "issue %s: resetting tx operation: %s for hash: %s" issue-id type tx-hash) - (db-bounties/reset-tx-hash! tx-hash type) - (when (= tx-hash (:tx-hash @eth/nonce-being-mined)) - (log/infof "issue %s: reset nonce" issue-id) - (reset! eth/nonce-being-mined nil))) - (when (and (:timestamp @eth/nonce-being-mined) - (t/before? (:timestamp @eth/nonce-being-mined) (t/minus (t/now) (t/minutes 5)))) - (log/errorf "nonce-being-mined not present in DB and unmined for 5 minutes, force reset. Tx hash: %s, type: %s" - (:tx-hash @eth/nonce-being-mined) (:type @eth/nonce-being-mined)) - (reset! eth/nonce-being-mined nil)) + (eth/prune-txs! (issues/unmined-txs)) (log/info "Exit check-tx-receipts")) (defn wrap-in-try-catch [func]