From 74a2a8060a22421696a1fed5658ab50b2097399b Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Fri, 13 Apr 2018 16:47:24 +0300 Subject: [PATCH 01/18] Throw exception if attempting to create tx with the same nonce --- src/clj/commiteth/bounties.clj | 5 +++-- src/clj/commiteth/eth/core.clj | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/clj/commiteth/bounties.clj b/src/clj/commiteth/bounties.clj index 2be8511..d1ee19d 100644 --- a/src/clj/commiteth/bounties.clj +++ b/src/clj/commiteth/bounties.clj @@ -23,7 +23,7 @@ (defn deploy-contract [owner owner-address repo issue-id issue-number] (if (empty? owner-address) (log/errorf "issue %s: Unable to deploy bounty contract because repo owner has no Ethereum addres" issue-id) - (do + (try (log/infof "issue %s: Deploying contract to %s" issue-id owner-address) (if-let [transaction-hash (multisig/deploy-multisig owner-address)] (do @@ -36,7 +36,8 @@ (log/infof "issue %s: post-deploying-comment response: %s" issue-id resp) (issues/update-comment-id issue-id comment-id)) (issues/update-transaction-hash issue-id transaction-hash)) - (log/errorf "issue %s Failed to deploy contract to %s" issue-id owner-address))))) + (log/errorf "issue %s Failed to deploy contract to %s" issue-id owner-address)) + (catch Exception ex (log/errorf ex "issue %s: deploy-contract exception" issue-id))))) (defn add-bounty-for-issue [repo repo-id issue] (let [{issue-id :id diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index f70374d..4250153 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -53,18 +53,27 @@ (throw (ex-info "Make sure you provided proper credentials in appropriate resources/config.edn" {:password password :file-path file-path})))))) +(defn get-nonce [] + (let [current-nonce (atom nil)] + (fn [] + (let [nonce (.. (.ethGetTransactionCount (create-web3j) + (env :eth-account) + DefaultBlockParameterName/LATEST) + sendAsync + get + getTransactionCount)] + (if (= nonce @current-nonce) + (throw (Exception. (str "Attempting to create transaction with the same nonce: " nonce))) + (swap! current-nonce (constantly nonce))) + (log/info "Current nonce:" nonce) + nonce)))) + (defn get-signed-tx [gas-price gas-limit to data] "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 [web3j (create-web3j) - nonce (.. (.ethGetTransactionCount web3j - (env :eth-account) - DefaultBlockParameterName/LATEST) - sendAsync - get - getTransactionCount) + (let [nonce ((get-nonce)) tx (RawTransaction/createTransaction nonce gas-price From b3e7936caffd88af15d249410b470c21995b0887 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Fri, 13 Apr 2018 17:03:22 +0300 Subject: [PATCH 02/18] Remove superfluous logging statement --- src/clj/commiteth/eth/core.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 4250153..2f6bfe5 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -65,7 +65,6 @@ (if (= nonce @current-nonce) (throw (Exception. (str "Attempting to create transaction with the same nonce: " nonce))) (swap! current-nonce (constantly nonce))) - (log/info "Current nonce:" nonce) nonce)))) (defn get-signed-tx [gas-price gas-limit to data] From b20bbb08cf0ec8f4c266ece28ce2b1880a56e363 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Mon, 16 Apr 2018 11:58:18 +0300 Subject: [PATCH 03/18] Fix lame mistake with get-nonce closure invocation --- src/clj/commiteth/eth/core.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 2f6bfe5..c85df48 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -66,13 +66,14 @@ (throw (Exception. (str "Attempting to create transaction with the same nonce: " nonce))) (swap! current-nonce (constantly nonce))) nonce)))) +(def get-nonce-fn (get-nonce)) (defn get-signed-tx [gas-price gas-limit to data] "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 [nonce ((get-nonce)) + (let [nonce (get-nonce-fn) tx (RawTransaction/createTransaction nonce gas-price From c6fadaad1a0f787090772f005ff9ff1914ef1d72 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Tue, 17 Apr 2018 20:32:02 +0300 Subject: [PATCH 04/18] Re-use dropped nonces --- src/clj/commiteth/eth/core.clj | 58 ++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index c85df48..9601564 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -53,28 +53,32 @@ (throw (ex-info "Make sure you provided proper credentials in appropriate resources/config.edn" {:password password :file-path file-path})))))) -(defn get-nonce [] - (let [current-nonce (atom nil)] - (fn [] - (let [nonce (.. (.ethGetTransactionCount (create-web3j) - (env :eth-account) - DefaultBlockParameterName/LATEST) - sendAsync - get - getTransactionCount)] - (if (= nonce @current-nonce) - (throw (Exception. (str "Attempting to create transaction with the same nonce: " nonce))) - (swap! current-nonce (constantly nonce))) - nonce)))) -(def get-nonce-fn (get-nonce)) +(def highest-nonce (atom nil)) +(def nonces-dropped (atom (clojure.lang.PersistentQueue/EMPTY))) -(defn get-signed-tx [gas-price gas-limit to data] + +(defn get-nonce [] + (if (seq @nonces-dropped) + (let [nonce (peek @nonces-dropped)] + (swap! nonces-dropped pop) + nonce) + (let [nonce (.. (.ethGetTransactionCount (create-web3j) + (env :eth-account) + DefaultBlockParameterName/LATEST) + sendAsync + get + getTransactionCount)] + (if (or (nil? @highest-nonce) + (> nonce @highest-nonce)) + (reset! highest-nonce nonce) + (swap! highest-nonce inc))))) + +(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 [nonce (get-nonce-fn) - tx (RawTransaction/createTransaction + (let [tx (RawTransaction/createTransaction nonce gas-price gas-limit @@ -155,7 +159,7 @@ (cond ;; Ignore any responses that have mismatching request ID (not= (:id result) request-id) - (log/error "Geth returned an invalid json-rpc request ID, ignoring response") + (throw (ex-info "Geth returned an invalid json-rpc request ID, ignoring response")) ;; If request ID matches but contains error, throw (:error result) @@ -266,16 +270,22 @@ (merge {:to contract})) 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) + (hex->big-integer gas) + contract + data + nonce) (assoc params :gas gas))] (if (offline-signing?) - (eth-rpc - "eth_sendRawTransaction" - [params]) + (try + (eth-rpc + "eth_sendRawTransaction" + [params]) + (catch Throwable ex + (swap! nonces-dropped conj nonce) + (throw ex))) (eth-rpc "personal_sendTransaction" [params (eth-password)])))) From f50638552f06811de82b3f2e5b8b0a2c6b5dc407 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 19 Apr 2018 16:21:08 +0300 Subject: [PATCH 05/18] Fix web3j obj reference --- src/clj/commiteth/eth/core.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index d9455e6..1d0142a 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -61,7 +61,7 @@ (let [nonce (peek @nonces-dropped)] (swap! nonces-dropped pop) nonce) - (let [nonce (.. (.ethGetTransactionCount (create-web3j) + (let [nonce (.. (.ethGetTransactionCount @web3j-obj (env :eth-account) DefaultBlockParameterName/LATEST) sendAsync From 855e2d83f0b3713c53235a3fea904a456bd7abe3 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 19 Apr 2018 17:07:30 +0300 Subject: [PATCH 06/18] Convert nonce to bigint after inc --- src/clj/commiteth/eth/core.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 1d0142a..7ddb49d 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -70,7 +70,7 @@ (if (or (nil? @highest-nonce) (> nonce @highest-nonce)) (reset! highest-nonce nonce) - (swap! highest-nonce inc))))) + (swap! highest-nonce (comp biginteger inc)))))) (defn get-signed-tx [gas-price gas-limit to data nonce] "Create a sign a raw transaction. From 183a50dc94fab565ca8794edc07fadf322079e03 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 19 Apr 2018 23:21:07 +0300 Subject: [PATCH 07/18] Include unmined contracts in the output of pending-contracts query --- resources/sql/queries.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 75d73e4..b40e88c 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -246,6 +246,7 @@ WHERE pr_id = :pr_id; SELECT i.issue_id AS issue_id, i.issue_number AS issue_number, + i.transaction_hash AS transaction_hash, r.owner AS owner, u.address AS owner_address, r.repo AS repo @@ -253,7 +254,7 @@ FROM issues i, users u, repositories r WHERE r.user_id = u.id AND i.repo_id = r.repo_id -AND i.transaction_hash IS NULL +AND (i.transaction_hash IS NULL OR updated > now() - interval '1 hour') AND i.contract_address IS NULL; From 03cf011f019ef75289e6dfb0dcbcacfcedd33ed4 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Mon, 23 Apr 2018 19:36:33 +0300 Subject: [PATCH 08/18] Add check-tx-receipts thread; allow only one tx submission at a time --- resources/sql/queries.sql | 47 ++++++++++++++++++++++- src/clj/commiteth/db/bounties.clj | 9 +++++ src/clj/commiteth/eth/core.clj | 46 ++++++++++++---------- src/clj/commiteth/eth/multisig_wallet.clj | 3 ++ src/clj/commiteth/scheduler.clj | 21 ++++++++++ 5 files changed, 103 insertions(+), 23 deletions(-) diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index b40e88c..d8efbd2 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -145,7 +145,8 @@ RETURNING repo_id, issue_id, issue_number, title, commit_sha, contract_address; -- :name update-transaction-hash :! :n -- :doc updates transaction-hash for a given issue UPDATE issues -SET transaction_hash = :transaction_hash +SET transaction_hash = :transaction_hash, + updated = timezone('utc'::text, now()) WHERE issue_id = :issue_id; @@ -241,6 +242,48 @@ WHERE pr_id = :pr_id; -- Bounties ------------------------------------------------------------------------ +-- :name unmined-tx-hashes :? :* +-- :doc hashes that haven't been mined for some time +SELECT + CASE WHEN transaction_hash is not null and contract_address is null + THEN transaction_hash + WHEN execute_hash is not null and confirm_hash is null + THEN execute_hash + WHEN watch_hash is not null + THEN watch_hash + END + AS tx_hash, + CASE WHEN transaction_hash is not null and contract_address is null + THEN 'deploy' + WHEN execute_hash is not null and confirm_hash is null + THEN 'execute' + WHEN watch_hash is not null + THEN 'watch' + END + AS type, + issue_id +FROM issues +WHERE updated < timezone('utc'::text, now()) - interval '5 minutes' +AND (transaction_hash is not null and contract_address is null + OR execute_hash is not null and confirm_hash is null + OR watch_hash is not null); + + +-- :name reset-tx-hash! :! :n +-- :doc reset tx hash if it hasn't been mined for some time +UPDATE issues + SET + --~ (when (= (:type params) "deploy") "transaction_hash") + --~ (when (= (:type params) "execute") "execute_hash") + --~ (when (= (:type params) "watch") "watch_hash") + = null + WHERE + --~ (when (= (:type params) "deploy") "transaction_hash") + --~ (when (= (:type params) "execute") "execute_hash") + --~ (when (= (:type params) "watch") "watch_hash") + = :tx-hash + + -- :name pending-contracts :? :* -- :doc bounty issues where deploy contract has failed SELECT @@ -254,7 +297,7 @@ FROM issues i, users u, repositories r WHERE r.user_id = u.id AND i.repo_id = r.repo_id -AND (i.transaction_hash IS NULL OR updated > now() - interval '1 hour') +AND i.transaction_hash IS NULL AND i.contract_address IS NULL; diff --git a/src/clj/commiteth/db/bounties.clj b/src/clj/commiteth/db/bounties.clj index ff1d14e..2e6c8f0 100644 --- a/src/clj/commiteth/db/bounties.clj +++ b/src/clj/commiteth/db/bounties.clj @@ -100,3 +100,12 @@ [] (jdbc/with-db-connection [con-db *db*] (db/bounties-activity con-db))) + +(defn unmined-tx-hashes [] + (jdbc/with-db-connection [con-db *db*] + (db/unmined-tx-hashes con-db))) + +(defn reset-tx-hash! + [tx-hash type] + (jdbc/with-db-connection [con-db *db*] + (db/reset-tx-hash! con-db {:tx-hash tx-hash :type type}))) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 7ddb49d..c9943fd 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -7,6 +7,7 @@ [taoensso.tufte :as tufte :refer (defnp p profiled profile)] [clojure.tools.logging :as log] [clojure.string :as str] + [clj-time.core :as t] [mount.core :as mount] [pandect.core :as pandect] [commiteth.util.util :refer [json-api-request]]) @@ -51,26 +52,20 @@ (throw (ex-info "Make sure you provided proper credentials in appropriate resources/config.edn" {:password password :file-path file-path})))))) - -(def highest-nonce (atom nil)) -(def nonces-dropped (atom (clojure.lang.PersistentQueue/EMPTY))) - +(defrecord TxNonce [tx-hash nonce type timestamp]) +(def nonce-being-mined (atom nil)) (defn get-nonce [] - (if (seq @nonces-dropped) - (let [nonce (peek @nonces-dropped)] - (swap! nonces-dropped pop) - nonce) (let [nonce (.. (.ethGetTransactionCount @web3j-obj (env :eth-account) - DefaultBlockParameterName/LATEST) + DefaultBlockParameterName/PENDING) sendAsync get getTransactionCount)] - (if (or (nil? @highest-nonce) - (> nonce @highest-nonce)) - (reset! highest-nonce nonce) - (swap! highest-nonce (comp biginteger inc)))))) + (if (or (nil? @nonce-being-mined) + (> 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)))))) (defn get-signed-tx [gas-price gas-limit to data nonce] "Create a sign a raw transaction. @@ -248,8 +243,13 @@ (defn get-transaction-receipt [hash] - (eth-rpc {:method "eth_getTransactionReceipt" - :params [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)) (defn format-call-params [method-id & params] @@ -263,7 +263,7 @@ :params [{:to contract :data data} "latest"]}))) (defn execute - [{:keys [from contract method-id gas-limit params internal-tx-id]}] + [{:keys [from contract method-id gas-limit params internal-tx-id type]}] {:pre [(string? method-id)]} (let [data (apply format-call-params method-id params) gas-price (gas-price) @@ -288,12 +288,16 @@ (assoc params :gas gas))] (if (offline-signing?) (try - (eth-rpc - {:method "eth_sendRawTransaction" - :params [params] - :internal-tx-id internal-tx-id}) + (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 - (swap! nonces-dropped conj nonce) + (reset! nonce-being-mined nil) (throw ex))) (eth-rpc {:method "personal_sendTransaction" diff --git a/src/clj/commiteth/eth/multisig_wallet.clj b/src/clj/commiteth/eth/multisig_wallet.clj index 874c113..fd1bbef 100644 --- a/src/clj/commiteth/eth/multisig_wallet.clj +++ b/src/clj/commiteth/eth/multisig_wallet.clj @@ -43,6 +43,7 @@ [{:keys [owner internal-tx-id]}] {:pre [(string? owner) (string? 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) @@ -93,6 +94,7 @@ (: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) @@ -111,6 +113,7 @@ (let [token-address (get-token-address token)] (assert token-address) (eth/execute {:internal-tx-id (str "watch-token-" (System/currentTimeMillis) "-" bounty-addr) + :type "watch" :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 4299c49..f59e2b3 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -89,6 +89,7 @@ (p :deploy-pending-contracts (doseq [{issue-id :issue_id issue-number :issue_number + transaction-hash :transaction_hash owner :owner owner-address :owner_address repo :repo} (db-bounties/pending-contracts)] @@ -371,6 +372,25 @@ (log/error ex "issue %s: update-balances exception" issue-id))))) (log/info "Exit update-balances")) +(defn check-tx-receipts + "At all times, there should be no more than one unmined tx hash, + 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)) + (log/info "Exit check-tx-receipts")) (defn wrap-in-try-catch [func] (try @@ -394,6 +414,7 @@ update-confirm-hash update-payout-receipt update-watch-hash + check-tx-receipts self-sign-bounty ]) (log/info "run-1-min-interval-tasks done"))) From 1f056592588c7ccf2b3e7af98b2dc3b3956d08db Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Mon, 23 Apr 2018 19:37:28 +0300 Subject: [PATCH 09/18] Remove transaction-hash local in deploy-pending-contracts --- src/clj/commiteth/scheduler.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index f59e2b3..aecfc92 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -89,7 +89,6 @@ (p :deploy-pending-contracts (doseq [{issue-id :issue_id issue-number :issue_number - transaction-hash :transaction_hash owner :owner owner-address :owner_address repo :repo} (db-bounties/pending-contracts)] From ea406387dd9f10ce0a5e3bbad3b544c4a71651b1 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 26 Apr 2018 17:17:00 +0300 Subject: [PATCH 10/18] 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] From 6f5dcd57ae82117b216ff312eba838f380f87092 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 26 Apr 2018 17:17:22 +0300 Subject: [PATCH 11/18] Remove obsolete update-execute-hash query --- resources/sql/queries.sql | 7 ------- 1 file changed, 7 deletions(-) diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 3028c73..1c207ae 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -348,13 +348,6 @@ AND u.id = p.user_id AND i.payout_receipt IS NULL AND i.payout_hash IS NOT NULL; --- :name update-execute-hash :! :n --- :doc updates issue with execute transaction hash -UPDATE issues -SET execute_hash = :execute_hash, -updated = timezone('utc'::text, now()) -WHERE issue_id = :issue_id; - -- :name update-winner-login :! :n UPDATE issues SET winner_login = :winner_login From 6234bfcb25441d0bde31512717048945775032b8 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 26 Apr 2018 17:26:43 +0300 Subject: [PATCH 12/18] Add some docstrings --- src/clj/commiteth/eth/core.clj | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 67f3002..d158199 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -63,11 +63,20 @@ (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]) + (try-reserve-nonce [this] + "Fetch current nonce via eth_getTransactionCount + and either return it if it is not in use yet, + or throw an exception") + (drop-nonce [this nonce] + "If tx execution returned with an error, + release previously reserved nonce") + (track-tx [this tx-info] + "Record tx data after successful submission") + (untrack-tx [this tx-info] + "Mark tx as successfully closed") + (prune-txs [this txs] + "Release nonces related to txs param + and the ones that have expired (haven't been in a certain time)") ) (defrecord SequentialTxTracker [current-tx] @@ -95,17 +104,24 @@ ) (def tx-tracker (SequentialTxTracker. (atom nil))) -(defn track-tx! [{:keys [issue-id tx-hash type] +(defn track-tx! + "Store tx data in tx-tracker and DB" + [{: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] +(defn untrack-tx! + "Mark tx data stored in tx-tracker and DB as closed" + [{: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] + "Release nonces related to unmined txs, + and set relevant DB fields to null thereby + marking them as candidates for re-execution" (doseq [{issue-id :issue_id tx-hash :tx_hash type :type} unmined-txs] @@ -114,7 +130,6 @@ (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 From dc9fc5e63395ffc34701f72006bae131c1dad67f Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 26 Apr 2018 17:56:17 +0300 Subject: [PATCH 13/18] Minor docstring fix for TxTracker --- src/clj/commiteth/eth/core.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index d158199..54582e9 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -76,7 +76,7 @@ "Mark tx as successfully closed") (prune-txs [this txs] "Release nonces related to txs param - and the ones that have expired (haven't been in a certain time)") + and the ones that have expired (haven't been marked as closed after a certain timeout)") ) (defrecord SequentialTxTracker [current-tx] From b7f996c2868652b70a41a1c4f56374b6ca5a5732 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 26 Apr 2018 19:08:30 +0300 Subject: [PATCH 14/18] Add eth.web3j and eth.tracker namespaces --- src/clj/commiteth/bounties.clj | 3 +- src/clj/commiteth/eth/core.clj | 162 +++------------------- src/clj/commiteth/eth/multisig_wallet.clj | 5 +- src/clj/commiteth/eth/token_registry.clj | 9 +- src/clj/commiteth/eth/tracker.clj | 98 +++++++++++++ src/clj/commiteth/eth/web3j.clj | 46 ++++++ src/clj/commiteth/scheduler.clj | 25 ++-- 7 files changed, 183 insertions(+), 165 deletions(-) create mode 100644 src/clj/commiteth/eth/tracker.clj create mode 100644 src/clj/commiteth/eth/web3j.clj diff --git a/src/clj/commiteth/bounties.clj b/src/clj/commiteth/bounties.clj index 7ea5faf..0eb77e1 100644 --- a/src/clj/commiteth/bounties.clj +++ b/src/clj/commiteth/bounties.clj @@ -4,6 +4,7 @@ [commiteth.db.repositories :as repos] [commiteth.db.comment-images :as comment-images] [commiteth.eth.core :as eth] + [commiteth.eth.tracker :as tracker] [commiteth.github.core :as github] [commiteth.eth.multisig-wallet :as multisig] [commiteth.util.png-rendering :as png-rendering] @@ -36,7 +37,7 @@ comment-id (:id resp)] (log/infof "issue %s: post-deploying-comment response: %s" issue-id resp) (issues/update-comment-id issue-id comment-id)) - (eth/track-tx! tx-info)) + (tracker/track-tx! tx-info)) (log/errorf "issue %s Failed to deploy contract to %s" issue-id owner-address)) (catch Exception ex (log/errorf ex "issue %s: deploy-contract exception" issue-id))))) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 54582e9..71e491e 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -1,27 +1,17 @@ (ns commiteth.eth.core (:require [clojure.data.json :as json] [org.httpkit.client :refer [post]] - [commiteth.db.issues :as issues] [clojure.java.io :as io] [commiteth.config :refer [env]] + [commiteth.eth.web3j :refer [get-signed-tx]] [clojure.string :refer [join]] [taoensso.tufte :as tufte :refer (defnp p profiled profile)] [clojure.tools.logging :as log] + [commiteth.eth.tracker :as tracker] + [clj-time.core :as t] [clojure.string :as str] [pandect.core :as pandect] - [mount.core :as mount] - [clj-time.core :as t] - [commiteth.util.util :refer [json-api-request]]) - (:import [org.web3j - protocol.Web3j - protocol.http.HttpService - protocol.core.DefaultBlockParameterName - protocol.core.methods.response.EthGetTransactionCount - protocol.core.methods.request.RawTransaction - utils.Numeric - crypto.Credentials - crypto.TransactionEncoder - crypto.WalletUtils])) + [commiteth.util.util :refer [json-api-request]])) (defn eth-rpc-url [] (env :eth-rpc-url "http://localhost:8545")) (defn eth-account [] (:eth-account env)) @@ -30,118 +20,6 @@ (defn auto-gas-price? [] (env :auto-gas-price false)) (defn offline-signing? [] (env :offline-signing true)) -(def web3j-obj - (delay (Web3j/build (HttpService. (eth-rpc-url))))) - -(def creds-obj (atom nil)) - -(defn wallet-file-path [] - (env :eth-wallet-file)) - -(defn wallet-password [] - (env :eth-password)) - -(defn creds [] - (or @creds-obj - (let [password (wallet-password) - file-path (wallet-file-path)] - (if (and password file-path) - (swap! creds-obj - (constantly (WalletUtils/loadCredentials - password - file-path))) - (throw (ex-info "Make sure you provided proper credentials in appropriate resources/config.edn" - {:password password :file-path file-path})))))) - -(defn get-nonce [] - (.. (.ethGetTransactionCount @web3j-obj - (env :eth-account) - DefaultBlockParameterName/PENDING) - sendAsync - get - getTransactionCount)) - - -(defprotocol TxTracker - (try-reserve-nonce [this] - "Fetch current nonce via eth_getTransactionCount - and either return it if it is not in use yet, - or throw an exception") - (drop-nonce [this nonce] - "If tx execution returned with an error, - release previously reserved nonce") - (track-tx [this tx-info] - "Record tx data after successful submission") - (untrack-tx [this tx-info] - "Mark tx as successfully closed") - (prune-txs [this txs] - "Release nonces related to txs param - and the ones that have expired (haven't been marked as closed after a certain timeout)") - ) - -(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! - "Store tx data in tx-tracker and DB" - [{: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! - "Mark tx data stored in tx-tracker and DB as closed" - [{: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] - "Release nonces related to unmined txs, - and set relevant DB fields to null thereby - marking them as candidates for re-execution" - (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" - (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 "Get max of average and average_calc from gas station and use that as gas price. average_calc is computed from a larger time period than average, @@ -212,7 +90,8 @@ (cond ;; Ignore any responses that have mismatching request ID (not= (:id result) request-id) - (throw (ex-info "Geth returned an invalid json-rpc request ID, ignoring response")) + (throw (ex-info "Geth returned an invalid json-rpc request ID, ignoring response" + {:result result})) ;; If request ID matches but contains error, throw (:error result) @@ -328,15 +207,17 @@ contract (merge {:to contract})))) -(defmulti execute (fn [arg] (offline-signing?))) +(defmulti execute (fn [_] (if (offline-signing?) + :with-tx-signing + :no-tx-signing))) -(defmethod execute true +(defmethod execute :with-tx-signing [{: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) + nonce (tracker/try-reserve-nonce!) gas-price (gas-price) params (construct-params (assoc args :gas-price gas-price)) gas (or gas-limit (estimate-gas from contract (:value params) params)) @@ -347,11 +228,11 @@ nonce) tx-hash (try (eth-rpc - {:method "eth_sendRawTransaction" - :params [params] - :internal-tx-id internal-tx-id}) + {:method "eth_sendRawTransaction" + :params [params] + :internal-tx-id internal-tx-id}) (catch Throwable ex - (drop-nonce tx-tracker nonce) + (tracker/drop-nonce! nonce) (throw ex)))] {:tx-hash tx-hash :issue-id issue-id @@ -359,7 +240,7 @@ :nonce nonce :timestamp (t/now)})) -(defmethod execute false +(defmethod execute :no-tx-signing [{:keys [from contract method-id gas-limit params internal-tx-id] :as args}] {:pre [(string? method-id)]} @@ -433,14 +314,3 @@ (filter true?) (empty?))) true)))) - -(mount/defstate - eth-core - :start - (do - (swap! creds-obj (constantly nil)) - (log/info "eth/core started")) - :stop - (log/info "eth/core stopped")) - - diff --git a/src/clj/commiteth/eth/multisig_wallet.clj b/src/clj/commiteth/eth/multisig_wallet.clj index 09c4d8f..5b425a7 100644 --- a/src/clj/commiteth/eth/multisig_wallet.clj +++ b/src/clj/commiteth/eth/multisig_wallet.clj @@ -2,6 +2,7 @@ (:require [commiteth.eth.core :as eth] [commiteth.config :refer [env]] [clojure.tools.logging :as log] + [commiteth.eth.web3j :refer [web3j-obj creds-obj]] [taoensso.tufte :as tufte :refer (defnp p profiled profile)] [commiteth.eth.token-data :as token-data]) (:import [org.web3j @@ -119,8 +120,8 @@ (defn load-bounty-contract [addr] (MultiSigTokenWallet/load addr - @eth/web3j-obj - (eth/creds) + @web3j-obj + @creds-obj (eth/gas-price) (BigInteger/valueOf 500000))) diff --git a/src/clj/commiteth/eth/token_registry.clj b/src/clj/commiteth/eth/token_registry.clj index c7aeec7..a80a0f0 100644 --- a/src/clj/commiteth/eth/token_registry.clj +++ b/src/clj/commiteth/eth/token_registry.clj @@ -1,6 +1,7 @@ (ns commiteth.eth.token-registry (:require [commiteth.eth.core :as eth] [commiteth.config :refer [env]] + [commiteth.eth.web3j :refer [web3j-obj creds-obj]] [clojure.tools.logging :as log]) (:import [org.web3j abi.datatypes.generated.Uint256 @@ -22,8 +23,8 @@ (defn- load-tokenreg-contract [addr] (TokenReg/load addr - @eth/web3j-obj - (eth/creds) + @web3j-obj + @creds-obj (eth/gas-price) (BigInteger/valueOf 21000))) @@ -58,8 +59,8 @@ (defn deploy-parity-tokenreg "Deploy an instance of parity token-registry to current network" [] - (TokenReg/deploy @eth/web3j-obj - (eth/creds) + (TokenReg/deploy @web3j-obj + @creds-obj (eth/gas-price) (BigInteger/valueOf 4000000) ;; gas limit BigInteger/ZERO)) diff --git a/src/clj/commiteth/eth/tracker.clj b/src/clj/commiteth/eth/tracker.clj new file mode 100644 index 0000000..ca3ed45 --- /dev/null +++ b/src/clj/commiteth/eth/tracker.clj @@ -0,0 +1,98 @@ +(ns commiteth.eth.tracker + (:require [clojure.data.json :as json] + [org.httpkit.client :refer [post]] + [commiteth.db.issues :as issues] + [commiteth.config :refer [env]] + [commiteth.eth.web3j :refer [web3j-obj]] + [clojure.tools.logging :as log] + [clojure.string :as str] + [clj-time.core :as t]) + (:import [org.web3j + protocol.Web3j + protocol.http.HttpService + protocol.core.DefaultBlockParameterName])) + +(defn eth-rpc-url [] (env :eth-rpc-url "http://localhost:8545")) + +(defn get-nonce [] + (.. (.ethGetTransactionCount @web3j-obj + (env :eth-account) + DefaultBlockParameterName/PENDING) + sendAsync + get + getTransactionCount)) + +(defprotocol ITxTracker + (try-reserve-nonce [this] + "Fetch current nonce via eth_getTransactionCount + and either return it if it is not in use yet, + or throw an exception") + (drop-nonce [this nonce] + "If tx execution returned with an error, + release previously reserved nonce") + (track-tx [this tx-info] + "Record tx data after successful submission") + (untrack-tx [this tx-info] + "Mark tx as successfully mined") + (prune-txs [this txs] + "Release nonces related to txs param + and the ones that have expired (haven't been mined after a certain timeout)") + ) + +(defrecord SequentialTxTracker [current-tx] + ITxTracker + (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 try-reserve-nonce! [] + (try-reserve-nonce tx-tracker)) + +(defn drop-nonce! [nonce] + (drop-nonce tx-tracker nonce)) + +(defn track-tx! + "Store tx data in tx-tracker and DB" + [{: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! + "Mark tx data stored in tx-tracker and DB as successfully mined" + [{: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] + "Release nonces related to unmined txs, + and set relevant DB fields to null thereby + marking them as candidates for re-execution" + (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)) + diff --git a/src/clj/commiteth/eth/web3j.clj b/src/clj/commiteth/eth/web3j.clj new file mode 100644 index 0000000..975317c --- /dev/null +++ b/src/clj/commiteth/eth/web3j.clj @@ -0,0 +1,46 @@ +(ns commiteth.eth.web3j + (:require [commiteth.config :refer [env]] + [clojure.tools.logging :as log]) + (:import [org.web3j + protocol.Web3j + protocol.http.HttpService + protocol.core.methods.request.RawTransaction + utils.Numeric + crypto.TransactionEncoder + crypto.WalletUtils])) + +(defn eth-rpc-url [] (env :eth-rpc-url "http://localhost:8545")) + +(def web3j-obj + (delay (Web3j/build (HttpService. (eth-rpc-url))))) + +(defn wallet-file-path [] + (env :eth-wallet-file)) + +(defn wallet-password [] + (env :eth-password)) + +(def creds-obj + (delay + (let [password (wallet-password) + file-path (wallet-file-path)] + (if (and password file-path) + (WalletUtils/loadCredentials + password + file-path) + (throw (ex-info "Make sure you provided proper credentials in appropriate resources/config.edn" + {:password password :file-path file-path})))))) + +(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" + (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-obj) + (Numeric/toHexString))) + + + diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index be5b814..93905af 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -2,6 +2,7 @@ (:require [commiteth.eth.core :as eth] [commiteth.eth.multisig-wallet :as multisig] [commiteth.eth.token-data :as token-data] + [commiteth.eth.tracker :as tracker] [commiteth.github.core :as github] [commiteth.db.issues :as issues] [taoensso.tufte :as tufte :refer (defnp p profiled profile)] @@ -52,7 +53,7 @@ (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 [_ (eth/untrack-tx! {:issue-id issue-id + (let [_ (tracker/untrack-tx! {:issue-id issue-id :tx-hash transaction-hash :result contract-address :type :deploy}) @@ -133,7 +134,7 @@ :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) + (tracker/track-tx! tx-info) (db-bounties/update-winner-login issue-id winner-login) (github/update-merged-issue-comment owner repo @@ -160,10 +161,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) - (eth/untrack-tx! {:issue-id issue-id - :tx-hash execute-hash - :result confirm-hash - :type :execute}))) + (tracker/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")) @@ -178,10 +179,10 @@ (log/infof "issue %s: pending watch call %s" issue-id watch-hash) (try (when-let [receipt (eth/get-transaction-receipt watch-hash)] - (eth/untrack-tx! {:issue-id issue-id - :tx-hash watch-hash - :result nil - :type :watch})) + (tracker/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)))))) @@ -272,7 +273,7 @@ (let [tx-info (multisig/watch-token {:bounty-addr bounty-addr :token tla :internal-tx-id [:watch issue-id]})] - (eth/track-tx! tx-info))))))) + (tracker/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")) @@ -387,7 +388,7 @@ as we are executing txs sequentially" [] (log/info "In check-tx-receipts") - (eth/prune-txs! (issues/unmined-txs)) + (tracker/prune-txs! (issues/unmined-txs)) (log/info "Exit check-tx-receipts")) (defn wrap-in-try-catch [func] From c288335418d17648dc68b79a820c49b49dc7d102 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Thu, 26 Apr 2018 19:22:56 +0300 Subject: [PATCH 15/18] Check nonce in untrack-tx --- src/clj/commiteth/eth/tracker.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clj/commiteth/eth/tracker.clj b/src/clj/commiteth/eth/tracker.clj index ca3ed45..fec30cf 100644 --- a/src/clj/commiteth/eth/tracker.clj +++ b/src/clj/commiteth/eth/tracker.clj @@ -52,7 +52,8 @@ (track-tx [this tx-info] (reset! current-tx tx-info)) (untrack-tx [this tx-info] - (reset! current-tx nil)) + (when (= (:nonce tx-info) (:nonce @current-tx)) + (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) From dd7d4e7ab31452a8978ac110c8adf7573069c4bf Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Mon, 30 Apr 2018 13:32:52 +0300 Subject: [PATCH 16/18] Change re-deployment timeout from 5 to 10 minutes --- resources/sql/queries.sql | 2 +- src/clj/commiteth/eth/tracker.clj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 9bd3031..66d4614 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -238,7 +238,7 @@ SELECT AS type, issue_id FROM issues -WHERE updated < timezone('utc'::text, now()) - interval '5 minutes' +WHERE updated < timezone('utc'::text, now()) - interval '10 minutes' AND (transaction_hash is not null and contract_address is null OR execute_hash is not null and confirm_hash is null OR watch_hash is not null); diff --git a/src/clj/commiteth/eth/tracker.clj b/src/clj/commiteth/eth/tracker.clj index fec30cf..f46270e 100644 --- a/src/clj/commiteth/eth/tracker.clj +++ b/src/clj/commiteth/eth/tracker.clj @@ -57,8 +57,8 @@ (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" + (t/before? (:timestamp @current-tx) (t/minus (t/now) (t/minutes 10))))) + (log/errorf "Current nonce unmined for 10 minutes, force reset. Tx hash: %s, type: %s" (:tx-hash @current-tx) (:type @current-tx)) (reset! current-tx nil))) From 2ee5af98eb950f71cae8c06d05c3a65a505a7084 Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Mon, 30 Apr 2018 13:33:41 +0300 Subject: [PATCH 17/18] Change post-deploying-comment fn to re-use existing comment when redeploying --- src/clj/commiteth/bounties.clj | 9 ++------- src/clj/commiteth/github/core.clj | 26 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/clj/commiteth/bounties.clj b/src/clj/commiteth/bounties.clj index 0735d86..5ee96e8 100644 --- a/src/clj/commiteth/bounties.clj +++ b/src/clj/commiteth/bounties.clj @@ -31,13 +31,8 @@ :internal-tx-id [:deploy issue-id]})] (do (log/infof "issue %s: Contract deployed, transaction-hash: %s" issue-id (:tx-hash tx-info)) - (let [resp (github/post-deploying-comment owner - repo - issue-number - (:tx-hash tx-info)) - comment-id (:id resp)] - (log/infof "issue %s: post-deploying-comment response: %s" issue-id resp) - (issues/update-comment-id issue-id comment-id)) + (github/post-deploying-comment issue-id + (:tx-hash tx-info)) (tracker/track-tx! tx-info)) (log/errorf "issue %s Failed to deploy contract to %s" issue-id owner-address)) (catch Exception ex (log/errorf ex "issue %s: deploy-contract exception" issue-id))))) diff --git a/src/clj/commiteth/github/core.clj b/src/clj/commiteth/github/core.clj index a121461..ba0c89b 100644 --- a/src/clj/commiteth/github/core.clj +++ b/src/clj/commiteth/github/core.clj @@ -12,6 +12,7 @@ [clj-http.client :as http] [commiteth.config :refer [env]] [digest :refer [sha-256]] + [commiteth.db.issues :as db-issues] [clojure.tools.logging :as log] [cheshire.core :as json] [clojure.string :as str]) @@ -271,13 +272,6 @@ (learn-more-text)) eth-balance-str payee-login)) - -(defn post-deploying-comment - [owner repo issue-number tx-id] - (let [comment (generate-deploying-comment owner repo issue-number tx-id)] - (log/info "Posting comment to" (str owner "/" repo "/" issue-number) ":" comment) - (issues/create-comment owner repo issue-number comment (self-auth-params)))) - (defn make-patch-request [end-point positional query] (let [{:keys [auth oauth-token] :as query} query @@ -296,6 +290,24 @@ :otp))] (assoc req :body (json/generate-string (or raw-query proper-query))))) +(defn post-deploying-comment + [issue-id tx-id] + (let [{owner :owner + repo :repo + issue-number :issue_number + comment-id :comment_id} (db-issues/get-issue-by-id issue-id) + comment (generate-deploying-comment owner repo issue-number tx-id) ] + (log/info "Posting comment to" (str owner "/" repo "/" issue-number) ":" comment) + (if comment-id + (let [req (make-patch-request "repos/%s/%s/issues/comments/%s" + [owner repo comment-id] + (assoc (self-auth-params) :body comment))] + (tentacles/safe-parse (http/request req))) + (let [resp (issues/create-comment owner repo issue-number comment (self-auth-params))] + (db-issues/update-comment-id issue-id (:id resp)) + (log/infof "issue %s: post-deploying-comment response: %s" issue-id resp) + resp)))) + (defn update-comment "Update comment for an open bounty issue" [owner repo comment-id issue-number contract-address eth-balance eth-balance-str tokens] From 4ca3fcb83324ee1a8443f4c716af8f0af084f8fc Mon Sep 17 00:00:00 2001 From: Vitaliy Vlasov Date: Mon, 30 Apr 2018 14:17:42 +0300 Subject: [PATCH 18/18] Remove unneeded args from deploy-contract fn --- src/clj/commiteth/bounties.clj | 4 ++-- src/clj/commiteth/scheduler.clj | 15 ++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/clj/commiteth/bounties.clj b/src/clj/commiteth/bounties.clj index 5ee96e8..2473a2e 100644 --- a/src/clj/commiteth/bounties.clj +++ b/src/clj/commiteth/bounties.clj @@ -22,7 +22,7 @@ (let [labels (:labels issue)] (some #(= label-name (:name %)) labels))) -(defn deploy-contract [owner owner-address repo issue-id issue-number] +(defn deploy-contract [owner-address issue-id] (if (empty? owner-address) (log/errorf "issue %s: Unable to deploy bounty contract because repo owner has no Ethereum addres" issue-id) (try @@ -47,7 +47,7 @@ (log/debug "issue %s: Adding bounty for issue %s/%s - owner address: %s" issue-id repo issue-number owner-address) (if (= 1 created-issue) - (deploy-contract owner owner-address repo issue-id issue-number) + (deploy-contract owner-address issue-id) (log/debug "issue %s: Issue already exists in DB, ignoring")))) (defn maybe-add-bounty-for-issue [repo repo-id issue] diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index 6985786..4329bd4 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -94,13 +94,10 @@ [] (p :deploy-pending-contracts (doseq [{issue-id :issue_id - issue-number :issue_number - owner :owner - owner-address :owner_address - repo :repo} (db-bounties/pending-contracts)] + owner-address :owner_address} (db-bounties/pending-contracts)] (log/infof "issue %s: Trying to re-deploy failed bounty contract deployment" issue-id) (try - (bounties/deploy-contract owner owner-address repo issue-id issue-number) + (bounties/deploy-contract owner-address issue-id) (catch Throwable t (log/errorf t "issue %s: deploy-pending-contracts exception: %s" issue-id (ex-data t))))))) @@ -436,11 +433,11 @@ (mount/defstate scheduler :start (let [every-minute (rest - (periodic-seq (t/now) - (t/minutes 1))) + (periodic-seq (t/now) + (t/minutes 1))) every-10-minutes (rest - (periodic-seq (t/now) - (t/minutes 10))) + (periodic-seq (t/now) + (t/minutes 10))) error-handler (fn [e] (log/error "Scheduled task failed" e) (throw e))