Merge pull request #427 from status-im/fix/nonce-increment

Send txs sequentially; add thread to check for unmined txs
This commit is contained in:
Vitaliy Vlasov 2018-05-02 16:11:06 +03:00 committed by GitHub
commit 5e4a4bfbb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 399 additions and 264 deletions

View File

@ -145,35 +145,11 @@ RETURNING repo_id, issue_id, issue_number, title, commit_sha, contract_address;
-- :name update-transaction-hash :! :n -- :name update-transaction-hash :! :n
-- :doc updates transaction-hash for a given issue -- :doc updates transaction-hash for a given issue
UPDATE issues UPDATE issues
SET transaction_hash = :transaction_hash SET transaction_hash = :transaction_hash,
updated = timezone('utc'::text, now())
WHERE issue_id = :issue_id; WHERE issue_id = :issue_id;
-- TODO: this is terrible
-- :name update-contract-address :<! :1
-- :doc updates contract-address for a given issue
WITH t AS (
SELECT
i.issue_id AS issue_id,
i.issue_number AS issue_number,
i.title AS title,
i.transaction_hash AS transaction_hash,
i.contract_address AS contract_address,
i.comment_id AS comment_id,
i.repo_id AS repo_id,
r.owner AS owner,
r.repo AS repo
FROM issues i, repositories r
WHERE r.repo_id = i.repo_id
AND i.issue_id = :issue_id
)
UPDATE issues i
SET contract_address = :contract_address,
updated = timezone('utc'::text, now())
FROM t
WHERE i.issue_id = :issue_id
RETURNING t.issue_id, t.issue_number, t.title, t.transaction_hash, t.comment_id, i.contract_address, t.owner, t.repo, t.repo_id;
-- :name update-comment-id :! :n -- :name update-comment-id :! :n
-- :doc updates comment-id for a given issue -- :doc updates comment-id for a given issue
UPDATE issues UPDATE issues
@ -241,11 +217,61 @@ WHERE pr_id = :pr_id;
-- Bounties ------------------------------------------------------------------------ -- Bounties ------------------------------------------------------------------------
-- :name unmined-txs :? :*
-- :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 '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);
-- :name save-tx-info! :! :n
-- :doc save tx hash from receipt
UPDATE issues
SET
--~ (when (= (:type params) "deploy") "transaction_hash")
--~ (when (= (:type params) "execute") "execute_hash")
--~ (when (= (:type params) "watch") "watch_hash")
= :tx-hash,
updated = timezone('utc'::text, now())
WHERE issue_id=:issue-id
-- :name save-tx-result! :! :n
-- :doc save tx hash from receipt
UPDATE issues
SET
--~ (when (= (:type params) "deploy") "contract_address")
--~ (when (= (:type params) "execute") "confirm_hash")
--~ (when (= (:type params) "watch") "watch_hash")
= :result,
updated = timezone('utc'::text, now())
WHERE issue_id=:issue-id
-- :name pending-contracts :? :* -- :name pending-contracts :? :*
-- :doc bounty issues where deploy contract has failed -- :doc bounty issues where deploy contract has failed
SELECT SELECT
i.issue_id AS issue_id, i.issue_id AS issue_id,
i.issue_number AS issue_number, i.issue_number AS issue_number,
i.transaction_hash AS transaction_hash,
r.owner AS owner, r.owner AS owner,
u.address AS owner_address, u.address AS owner_address,
r.repo AS repo r.repo AS repo
@ -322,31 +348,11 @@ AND u.id = p.user_id
AND i.payout_receipt IS NULL AND i.payout_receipt IS NULL
AND i.payout_hash IS NOT NULL; AND i.payout_hash IS NOT NULL;
-- :name update-confirm-hash :! :n
-- :doc updates issue with confirmation hash
UPDATE issues
SET confirm_hash = :confirm_hash,
updated = timezone('utc'::text, now())
WHERE issue_id = :issue_id;
-- :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 -- :name update-winner-login :! :n
UPDATE issues UPDATE issues
SET winner_login = :winner_login SET winner_login = :winner_login
WHERE issue_id = :issue_id; WHERE issue_id = :issue_id;
-- :name update-watch-hash :! :n
-- :doc updates issue with watch transaction hash
UPDATE issues
SET watch_hash = :watch_hash
WHERE issue_id = :issue_id;
-- :name pending-watch-calls :? :* -- :name pending-watch-calls :? :*
-- :doc issues with a pending watch transaction -- :doc issues with a pending watch transaction
SELECT SELECT
@ -415,6 +421,23 @@ FROM issues
WHERE repo_id = :repo_id WHERE repo_id = :repo_id
AND issue_number = :issue_number; AND issue_number = :issue_number;
-- :name get-issue-by-id :? :1
-- :doc get issue from DB by issue-id
SELECT
i.issue_id AS issue_id,
i.issue_number AS issue_number,
i.is_open AS is_open,
i.winner_login AS winner_login,
i.commit_sha AS commit_sha,
i.title AS title,
i.comment_id AS comment_id,
i.repo_id AS repo_id,
r.owner AS owner,
r.repo AS repo
FROM issues i, repositories r
WHERE r.repo_id = i.repo_id
AND i.issue_id = :issue-id
-- :name open-bounties :? :* -- :name open-bounties :? :*

View File

@ -4,6 +4,7 @@
[commiteth.db.repositories :as repos] [commiteth.db.repositories :as repos]
[commiteth.db.comment-images :as comment-images] [commiteth.db.comment-images :as comment-images]
[commiteth.eth.core :as eth] [commiteth.eth.core :as eth]
[commiteth.eth.tracker :as tracker]
[commiteth.github.core :as github] [commiteth.github.core :as github]
[commiteth.eth.multisig-wallet :as multisig] [commiteth.eth.multisig-wallet :as multisig]
[commiteth.model.bounty :as bnt] [commiteth.model.bounty :as bnt]
@ -21,23 +22,18 @@
(let [labels (:labels issue)] (let [labels (:labels issue)]
(some #(= label-name (:name %)) labels))) (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) (if (empty? owner-address)
(log/errorf "issue %s: Unable to deploy bounty contract because repo owner has no Ethereum addres" issue-id) (log/errorf "issue %s: Unable to deploy bounty contract because repo owner has no Ethereum addres" issue-id)
(try (try
(log/infof "issue %s: Deploying contract to %s" issue-id owner-address) (log/infof "issue %s: Deploying contract to %s" issue-id owner-address)
(if-let [transaction-hash (multisig/deploy-multisig {:owner owner-address (if-let [tx-info (multisig/deploy-multisig {:owner owner-address
:internal-tx-id (str "contract-github-issue-" issue-id)})] :internal-tx-id [:deploy issue-id]})]
(do (do
(log/infof "issue %s: Contract deployed, transaction-hash: %s" issue-id transaction-hash) (log/infof "issue %s: Contract deployed, transaction-hash: %s" issue-id (:tx-hash tx-info))
(let [resp (github/post-deploying-comment owner (github/post-deploying-comment issue-id
repo (:tx-hash tx-info))
issue-number (tracker/track-tx! tx-info))
transaction-hash)
comment-id (:id resp)]
(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))))) (catch Exception ex (log/errorf ex "issue %s: deploy-contract exception" issue-id)))))
@ -51,7 +47,7 @@
(log/debug "issue %s: Adding bounty for issue %s/%s - owner address: %s" (log/debug "issue %s: Adding bounty for issue %s/%s - owner address: %s"
issue-id repo issue-number owner-address) issue-id repo issue-number owner-address)
(if (= 1 created-issue) (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")))) (log/debug "issue %s: Issue already exists in DB, ignoring"))))
(defn maybe-add-bounty-for-issue [repo repo-id issue] (defn maybe-add-bounty-for-issue [repo repo-id issue]

View File

@ -40,26 +40,12 @@
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/confirmed-payouts con-db))) (db/confirmed-payouts con-db)))
(defn update-confirm-hash
[issue-id confirm-hash]
(jdbc/with-db-connection [con-db *db*]
(db/update-confirm-hash con-db {:issue_id issue-id :confirm_hash confirm-hash})))
(defn update-execute-hash
[issue-id execute-hash]
(jdbc/with-db-connection [con-db *db*]
(db/update-execute-hash con-db {:issue_id issue-id :execute_hash execute-hash})))
(defn update-winner-login (defn update-winner-login
[issue-id login] [issue-id login]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/update-winner-login con-db {:issue_id issue-id :winner_login login}))) (db/update-winner-login con-db {:issue_id issue-id :winner_login login})))
(defn update-watch-hash
[issue-id watch-hash]
(jdbc/with-db-connection [con-db *db*]
(db/update-watch-hash con-db {:issue_id issue-id :watch_hash watch-hash})))
(defn pending-watch-calls (defn pending-watch-calls
[] []
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
@ -100,3 +86,5 @@
[] []
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/bounties-activity con-db))) (db/bounties-activity con-db)))

View File

@ -38,19 +38,25 @@
:title title}))) :title title})))
(defn update-transaction-hash (defn save-tx-info!
"Updates issue with transaction-hash" "Set transaction_hash, execute_hash or watch_hash depending on operation"
[issue-id transaction-hash] [issue-id tx-hash type-kw]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/update-transaction-hash con-db {:issue_id issue-id (db/save-tx-info! con-db {:issue-id issue-id
:transaction_hash transaction-hash}))) :tx-hash tx-hash
:type (name type-kw)})))
(defn update-contract-address (defn save-tx-result!
"Updates issue with contract-address" "Set contract_address, confirm_hash or watch_hash depending on operation"
[issue-id contract-address] [issue-id result type-kw]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/update-contract-address con-db {:issue_id issue-id (db/save-tx-result! con-db {:issue-id issue-id
:contract_address contract-address}))) :result result
:type (name type-kw)})))
(defn unmined-txs []
(jdbc/with-db-connection [con-db *db*]
(db/unmined-txs con-db)))
(defn update-comment-id (defn update-comment-id
"Updates issue with comment id" "Updates issue with comment id"
@ -103,3 +109,8 @@
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/get-issue con-db {:repo_id repo-id (db/get-issue con-db {:repo_id repo-id
:issue_number issue-number}))) :issue_number issue-number})))
(defn get-issue-by-id
[issue-id]
(jdbc/with-db-connection [con-db *db*]
(db/get-issue-by-id con-db {:issue-id issue-id})))

View File

@ -3,23 +3,15 @@
[org.httpkit.client :refer [post]] [org.httpkit.client :refer [post]]
[clojure.java.io :as io] [clojure.java.io :as io]
[commiteth.config :refer [env]] [commiteth.config :refer [env]]
[commiteth.eth.web3j :refer [get-signed-tx]]
[clojure.string :refer [join]] [clojure.string :refer [join]]
[taoensso.tufte :as tufte :refer (defnp p profiled profile)] [taoensso.tufte :as tufte :refer (defnp p profiled profile)]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[commiteth.eth.tracker :as tracker]
[clj-time.core :as t]
[clojure.string :as str] [clojure.string :as str]
[mount.core :as mount]
[pandect.core :as pandect] [pandect.core :as pandect]
[commiteth.util.util :refer [json-api-request]]) [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]))
(defn eth-rpc-url [] (env :eth-rpc-url "http://localhost:8545")) (defn eth-rpc-url [] (env :eth-rpc-url "http://localhost:8545"))
(defn eth-account [] (:eth-account env)) (defn eth-account [] (:eth-account env))
@ -28,83 +20,6 @@
(defn auto-gas-price? [] (env :auto-gas-price false)) (defn auto-gas-price? [] (env :auto-gas-price false))
(defn offline-signing? [] (env :offline-signing true)) (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-web3j-nonce [web3j-instance]
(.. (.ethGetTransactionCount web3j-instance (env :eth-account) DefaultBlockParameterName/LATEST)
sendAsync
get
getTransactionCount))
(defprotocol INonceTracker
"The reason we need this is that we might send mutliple identical
transactions (e.g. bounty contract deploy with same owner) shortly
after another. In this case web3j's TX counting only increases once
a transaction has been confirmed and so we send multiple identical
transactions with the same nonce.
Web3j also provides a TransactionManager that increases nonces but it
does not track nonces related to transactions and so as far as I understand
it might cause transactions to be executed twice if they are retried.
https://github.com/web3j/web3j/blob/d19855475aa6620a7e93523bd9ede26ca50ed042/core/src/main/java/org/web3j/tx/RawTransactionManager.java"
(get-nonce [this internal-tx-id]
"Return the to be used nonce for an OpenBounty Ethereum
transaction identified by `internal-tx-id`. As these IDs are stable
we can use them to use consistent nonces for the same transaction."))
(defrecord NonceTracker [state]
INonceTracker
(get-nonce [this internal-tx-id]
(let [prev-nonce (get @state internal-tx-id)
web3j-nonce (get-web3j-nonce @web3j-obj)
nonces (set (vals @state))
nonce (if (seq nonces)
(inc (apply max nonces))
web3j-nonce)]
(when prev-nonce
(log/warnf "%s: tx will be retried (prev-nonce: %s, new-nonce: %s, web3j-nonce: %s)"
internal-tx-id prev-nonce nonce web3j-nonce))
;; TODO this is a memory leak since tracking state is never pruned
;; Since we're not doing 1000s of transactions every day yet we can
;; probably defer worrying about this until a bit later
(swap! state assoc internal-tx-id nonce)
nonce)))
(def nonce-tracker
(->NonceTracker (atom {})))
(defn get-signed-tx [{:keys [gas-price gas-limit to data internal-tx-id]}]
"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 nonce-tracker internal-tx-id)]
(log/infof "%s: Signing nonce: %s, gas-price: %s, gas-limit: %s"
internal-tx-id nonce gas-price gas-limit)
(-> (RawTransaction/createTransaction (biginteger nonce) gas-price gas-limit to data)
(TransactionEncoder/signMessage (creds))
(Numeric/toHexString))))
(defn eth-gasstation-gas-price (defn eth-gasstation-gas-price
"Get max of average and average_calc from gas station and use that "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, as gas price. average_calc is computed from a larger time period than average,
@ -155,7 +70,9 @@
(defn eth-rpc (defn eth-rpc
[{:keys [method params internal-tx-id]}] [{:keys [method params internal-tx-id]}]
{:pre [(string? method) (some? params)]} {: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" body {:jsonrpc "2.0"
:method method :method method
:params params :params params
@ -165,21 +82,22 @@
response @(post (eth-rpc-url) options) response @(post (eth-rpc-url) options)
result (safe-read-str (:body response))] result (safe-read-str (:body response))]
(when internal-tx-id (when internal-tx-id
(log/infof "%s: eth-rpc %s" internal-tx-id method)) (log/infof "%s: eth-rpc %s" tx-id-str method))
(log/debugf "%s: eth-rpc req(%s) body: %s" internal-tx-id request-id body) (log/debugf "%s: eth-rpc req(%s) body: %s" tx-id-str request-id body)
(if internal-tx-id (if tx-id-str
(log/infof "%s: eth-rpc req(%s) result: %s" internal-tx-id request-id result) (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)) (log/debugf "no-tx-id: eth-rpc req(%s) result: %s" request-id result))
(cond (cond
;; Ignore any responses that have mismatching request ID ;; Ignore any responses that have mismatching request ID
(not= (:id result) 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"
{:result result}))
;; If request ID matches but contains error, throw ;; If request ID matches but contains error, throw
(:error result) (:error result)
(throw (throw
(ex-info (format "%s: Error submitting transaction via eth-rpc %s" (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))) (:error result)))
:else :else
@ -277,36 +195,70 @@
(eth-rpc {:method "eth_call" (eth-rpc {:method "eth_call"
:params [{:to contract :data data} "latest"]}))) :params [{:to contract :data data} "latest"]})))
(defn execute (defn construct-params
[{:keys [from contract method-id gas-limit params internal-tx-id]}] [{:keys [from contract method-id params gas-price]}]
{:pre [(string? method-id)]}
(let [data (apply format-call-params method-id params) (let [data (apply format-call-params method-id params)
gas-price (gas-price) value (format "0x%x" 0)]
value (format "0x%x" 0) (cond-> {:data data
params (cond-> {:data data
:from from :from from
:value value} :value value}
gas-price gas-price
(merge {:gasPrice (integer->hex gas-price)}) (merge {:gasPrice (integer->hex gas-price)})
contract contract
(merge {:to contract})) (merge {:to contract}))))
gas (or gas-limit (estimate-gas from contract value params))
params (if (offline-signing?) (defmulti execute (fn [_] (if (offline-signing?)
(get-signed-tx {:internal-tx-id internal-tx-id :with-tx-signing
:gas-price (biginteger gas-price) :no-tx-signing)))
:gas-limit (hex->big-integer gas)
:to contract
:data data}) (defmethod execute :with-tx-signing
(assoc params :gas gas))] [{:keys [from contract method-id gas-limit params internal-tx-id]
(if (offline-signing?) :as args}]
{:pre [(string? method-id)]}
(let [[type-kw issue-id] internal-tx-id
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))
params (get-signed-tx (biginteger gas-price)
(hex->big-integer gas)
contract
(:data params)
nonce)
tx-hash (try
(eth-rpc (eth-rpc
{:method "eth_sendRawTransaction" {:method "eth_sendRawTransaction"
:params [params] :params [params]
:internal-tx-id internal-tx-id}) :internal-tx-id internal-tx-id})
(eth-rpc (catch Throwable ex
(tracker/drop-nonce! nonce)
(throw ex)))]
{:tx-hash tx-hash
:issue-id issue-id
:type type-kw
:nonce nonce
:timestamp (t/now)}))
(defmethod execute :no-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
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" {:method "personal_sendTransaction"
:params [params (eth-password)] :params [params (eth-password)]
:internal-tx-id internal-tx-id})))) :internal-tx-id internal-tx-id})]
{:tx-hash tx-hash
:type type-kw
:timestamp (t/now)
:issue-id issue-id}
))
(defn hex-ch->num (defn hex-ch->num
[ch] [ch]
@ -362,14 +314,3 @@
(filter true?) (filter true?)
(empty?))) (empty?)))
true)))) true))))
(mount/defstate
eth-core
:start
(do
(swap! creds-obj (constantly nil))
(log/info "eth/core started"))
:stop
(log/info "eth/core stopped"))

View File

@ -2,6 +2,7 @@
(:require [commiteth.eth.core :as eth] (:require [commiteth.eth.core :as eth]
[commiteth.config :refer [env]] [commiteth.config :refer [env]]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[commiteth.eth.web3j :refer [web3j-obj creds-obj]]
[taoensso.tufte :as tufte :refer (defnp p profiled profile)] [taoensso.tufte :as tufte :refer (defnp p profiled profile)]
[commiteth.eth.token-data :as token-data]) [commiteth.eth.token-data :as token-data])
(:import [org.web3j (:import [org.web3j
@ -41,7 +42,7 @@
`internal-tx-id` is used to identify what issue this multisig is deployed `internal-tx-id` is used to identify what issue this multisig is deployed
for and manage nonces at a later point in time." for and manage nonces at a later point in time."
[{:keys [owner internal-tx-id]}] [{: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 (eth/execute {:internal-tx-id internal-tx-id
:from (eth/eth-account) :from (eth/eth-account)
:contract (factory-contract-addr) :contract (factory-contract-addr)
@ -87,7 +88,7 @@
(defn send-all (defn send-all
[{:keys [contract payout-address internal-tx-id]}] [{: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) (log/debug "multisig/send-all " contract payout-address internal-tx-id)
(let [params (eth/format-call-params (let [params (eth/format-call-params
(:withdraw-everything method-ids) (:withdraw-everything method-ids)
@ -106,11 +107,11 @@
(:address token-details))) (:address token-details)))
(defn watch-token (defn watch-token
[bounty-addr token] [{:keys [bounty-addr token internal-tx-id]}]
(log/debug "multisig/watch-token" bounty-addr token) (log/debug "multisig/watch-token" bounty-addr token)
(let [token-address (get-token-address token)] (let [token-address (get-token-address token)]
(assert token-address) (assert token-address)
(eth/execute {:internal-tx-id (str "watch-token-" (System/currentTimeMillis) "-" bounty-addr) (eth/execute {:internal-tx-id internal-tx-id
:from (eth/eth-account) :from (eth/eth-account)
:contract bounty-addr :contract bounty-addr
:method-id (:watch method-ids) :method-id (:watch method-ids)
@ -119,8 +120,8 @@
(defn load-bounty-contract [addr] (defn load-bounty-contract [addr]
(MultiSigTokenWallet/load addr (MultiSigTokenWallet/load addr
@eth/web3j-obj @web3j-obj
(eth/creds) @creds-obj
(eth/gas-price) (eth/gas-price)
(BigInteger/valueOf 500000))) (BigInteger/valueOf 500000)))

View File

@ -1,6 +1,7 @@
(ns commiteth.eth.token-registry (ns commiteth.eth.token-registry
(:require [commiteth.eth.core :as eth] (:require [commiteth.eth.core :as eth]
[commiteth.config :refer [env]] [commiteth.config :refer [env]]
[commiteth.eth.web3j :refer [web3j-obj creds-obj]]
[clojure.tools.logging :as log]) [clojure.tools.logging :as log])
(:import [org.web3j (:import [org.web3j
abi.datatypes.generated.Uint256 abi.datatypes.generated.Uint256
@ -22,8 +23,8 @@
(defn- load-tokenreg-contract [addr] (defn- load-tokenreg-contract [addr]
(TokenReg/load addr (TokenReg/load addr
@eth/web3j-obj @web3j-obj
(eth/creds) @creds-obj
(eth/gas-price) (eth/gas-price)
(BigInteger/valueOf 21000))) (BigInteger/valueOf 21000)))
@ -58,8 +59,8 @@
(defn deploy-parity-tokenreg (defn deploy-parity-tokenreg
"Deploy an instance of parity token-registry to current network" "Deploy an instance of parity token-registry to current network"
[] []
(TokenReg/deploy @eth/web3j-obj (TokenReg/deploy @web3j-obj
(eth/creds) @creds-obj
(eth/gas-price) (eth/gas-price)
(BigInteger/valueOf 4000000) ;; gas limit (BigInteger/valueOf 4000000) ;; gas limit
BigInteger/ZERO)) BigInteger/ZERO))

View File

@ -0,0 +1,99 @@
(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]
(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)
(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)))
)
(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))

View File

@ -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)))

View File

@ -12,6 +12,7 @@
[clj-http.client :as http] [clj-http.client :as http]
[commiteth.config :refer [env]] [commiteth.config :refer [env]]
[digest :refer [sha-256]] [digest :refer [sha-256]]
[commiteth.db.issues :as db-issues]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[cheshire.core :as json] [cheshire.core :as json]
[clojure.string :as str]) [clojure.string :as str])
@ -271,13 +272,6 @@
(learn-more-text)) (learn-more-text))
eth-balance-str payee-login)) 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] (defn make-patch-request [end-point positional query]
(let [{:keys [auth oauth-token] (let [{:keys [auth oauth-token]
:as query} query :as query} query
@ -296,6 +290,24 @@
:otp))] :otp))]
(assoc req :body (json/generate-string (or raw-query proper-query))))) (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 (defn update-comment
"Update comment for an open bounty issue" "Update comment for an open bounty issue"
[owner repo comment-id issue-number contract-address eth-balance eth-balance-str tokens] [owner repo comment-id issue-number contract-address eth-balance eth-balance-str tokens]

View File

@ -2,6 +2,7 @@
(:require [commiteth.eth.core :as eth] (:require [commiteth.eth.core :as eth]
[commiteth.eth.multisig-wallet :as multisig] [commiteth.eth.multisig-wallet :as multisig]
[commiteth.eth.token-data :as token-data] [commiteth.eth.token-data :as token-data]
[commiteth.eth.tracker :as tracker]
[commiteth.github.core :as github] [commiteth.github.core :as github]
[commiteth.db.issues :as issues] [commiteth.db.issues :as issues]
[taoensso.tufte :as tufte :refer (defnp p profiled profile)] [taoensso.tufte :as tufte :refer (defnp p profiled profile)]
@ -52,11 +53,14 @@
(when-let [receipt (eth/get-transaction-receipt transaction-hash)] (when-let [receipt (eth/get-transaction-receipt transaction-hash)]
(log/infof "issue %s: update-issue-contract-address: tx receipt: %s" issue-id receipt) (log/infof "issue %s: update-issue-contract-address: tx receipt: %s" issue-id receipt)
(if-let [contract-address (multisig/find-created-multisig-address receipt)] (if-let [contract-address (multisig/find-created-multisig-address receipt)]
(let [issue (issues/update-contract-address issue-id contract-address) (let [_ (tracker/untrack-tx! {:issue-id issue-id
:tx-hash transaction-hash
:result contract-address
:type :deploy})
{owner :owner {owner :owner
repo :repo repo :repo
comment-id :comment_id 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-str (eth/get-balance-eth contract-address 6)
balance-eth (read-string balance-eth-str)] balance-eth (read-string balance-eth-str)]
(log/infof "issue %s: Updating comment image" issue-id) (log/infof "issue %s: Updating comment image" issue-id)
@ -90,13 +94,10 @@
[] []
(p :deploy-pending-contracts (p :deploy-pending-contracts
(doseq [{issue-id :issue_id (doseq [{issue-id :issue_id
issue-number :issue_number owner-address :owner_address} (db-bounties/pending-contracts)]
owner :owner
owner-address :owner_address
repo :repo} (db-bounties/pending-contracts)]
(log/infof "issue %s: Trying to re-deploy failed bounty contract deployment" issue-id) (log/infof "issue %s: Trying to re-deploy failed bounty contract deployment" issue-id)
(try (try
(bounties/deploy-contract owner owner-address repo issue-id issue-number) (bounties/deploy-contract owner-address issue-id)
(catch Throwable t (catch Throwable t
(log/errorf t "issue %s: deploy-pending-contracts exception: %s" issue-id (ex-data t))))))) (log/errorf t "issue %s: deploy-pending-contracts exception: %s" issue-id (ex-data t)))))))
@ -132,11 +133,11 @@
tokens tokens
winner-login winner-login
true)) true))
(let [execute-hash (multisig/send-all {:contract contract-address (let [tx-info (multisig/send-all {:contract contract-address
:payout-address payout-address :payout-address payout-address
:internal-tx-id (str "payout-github-issue-" issue-id)})] :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 execute-hash) (log/infof "issue %s: Payout self-signed, called sign-all(%s) tx: %s" issue-id contract-address payout-address (:tx-hash tx-info))
(db-bounties/update-execute-hash issue-id execute-hash) (tracker/track-tx! tx-info)
(github/update-merged-issue-comment owner (github/update-merged-issue-comment owner
repo repo
comment-id comment-id
@ -162,7 +163,10 @@
(log/infof "issue %s: execution receipt for issue " issue-id receipt) (log/infof "issue %s: execution receipt for issue " issue-id receipt)
(when-let [confirm-hash (multisig/find-confirmation-tx-id receipt)] (when-let [confirm-hash (multisig/find-confirmation-tx-id receipt)]
(log/infof "issue %s: confirm hash:" issue-id confirm-hash) (log/infof "issue %s: confirm hash:" issue-id confirm-hash)
(db-bounties/update-confirm-hash issue-id confirm-hash))) (tracker/untrack-tx! {:issue-id issue-id
:tx-hash execute-hash
:result confirm-hash
:type :execute})))
(catch Throwable ex (catch Throwable ex
(log/errorf ex "issue %s: update-confirm-hash exception:" issue-id))))) (log/errorf ex "issue %s: update-confirm-hash exception:" issue-id)))))
(log/info "Exit update-confirm-hash")) (log/info "Exit update-confirm-hash"))
@ -177,7 +181,10 @@
(log/infof "issue %s: pending watch call %s" issue-id watch-hash) (log/infof "issue %s: pending watch call %s" issue-id watch-hash)
(try (try
(when-let [receipt (eth/get-transaction-receipt watch-hash)] (when-let [receipt (eth/get-transaction-receipt watch-hash)]
(db-bounties/update-watch-hash issue-id nil)) (tracker/untrack-tx! {:issue-id issue-id
:tx-hash watch-hash
:result nil
:type :watch}))
(catch Throwable ex (catch Throwable ex
(log/errorf ex "issue %s: update-watch-hash exception:" issue-id)))))) (log/errorf ex "issue %s: update-watch-hash exception:" issue-id))))))
@ -265,8 +272,10 @@
(when (and (nil? watch-hash) (when (and (nil? watch-hash)
(not= balance internal-balance)) (not= balance internal-balance))
(log/infof "bounty %s: balances not in sync, calling watch" bounty-addr) (log/infof "bounty %s: balances not in sync, calling watch" bounty-addr)
(let [hash (multisig/watch-token bounty-addr tla)] (let [tx-info (multisig/watch-token {:bounty-addr bounty-addr
(db-bounties/update-watch-hash issue-id hash))))))) :token tla
:internal-tx-id [:watch issue-id]})]
(tracker/track-tx! tx-info)))))))
(catch Throwable ex (catch Throwable ex
(log/error ex "bounty %s: update-bounty-token-balances exception" bounty-addr)))) (log/error ex "bounty %s: update-bounty-token-balances exception" bounty-addr))))
(log/info "Exit update-bounty-token-balances")) (log/info "Exit update-bounty-token-balances"))
@ -376,6 +385,13 @@
(log/error ex "issue %s: update-balances exception" issue-id))))) (log/error ex "issue %s: update-balances exception" issue-id)))))
(log/info "Exit update-balances")) (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")
(tracker/prune-txs! (issues/unmined-txs))
(log/info "Exit check-tx-receipts"))
(defn wrap-in-try-catch [func] (defn wrap-in-try-catch [func]
(try (try
@ -399,6 +415,7 @@
update-confirm-hash update-confirm-hash
update-payout-receipt update-payout-receipt
update-watch-hash update-watch-hash
check-tx-receipts
self-sign-bounty self-sign-bounty
]) ])
(log/info "run-1-min-interval-tasks done"))) (log/info "run-1-min-interval-tasks done")))