Add TxTracker protocol; introduce [un]track-tx! fns for generic tx

tracking
This commit is contained in:
Vitaliy Vlasov 2018-04-26 17:17:00 +03:00
parent 1f05659258
commit ea406387dd
7 changed files with 229 additions and 197 deletions

View File

@ -150,31 +150,6 @@ SET transaction_hash = :transaction_hash,
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
-- :doc updates comment-id for a given issue
UPDATE issues
@ -242,7 +217,7 @@ WHERE pr_id = :pr_id;
-- Bounties ------------------------------------------------------------------------
-- :name unmined-tx-hashes :? :*
-- :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
@ -268,22 +243,29 @@ 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
-- :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")
= null
WHERE
--~ (when (= (:type params) "deploy") "transaction_hash")
--~ (when (= (:type params) "execute") "execute_hash")
--~ (when (= (:type params) "watch") "watch_hash")
= :tx-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 :? :*
-- :doc bounty issues where deploy contract has failed
SELECT
@ -366,13 +348,6 @@ AND u.id = p.user_id
AND i.payout_receipt IS 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
@ -385,12 +360,6 @@ UPDATE issues
SET winner_login = :winner_login
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 :? :*
-- :doc issues with a pending watch transaction
SELECT
@ -459,6 +428,23 @@ FROM issues
WHERE repo_id = :repo_id
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 :? :*

View File

@ -25,18 +25,18 @@
(log/errorf "issue %s: Unable to deploy bounty contract because repo owner has no Ethereum addres" issue-id)
(try
(log/infof "issue %s: Deploying contract to %s" issue-id owner-address)
(if-let [transaction-hash (multisig/deploy-multisig {:owner owner-address
:internal-tx-id (str "contract-github-issue-" issue-id)})]
(if-let [tx-info (multisig/deploy-multisig {:owner owner-address
:internal-tx-id [:deploy issue-id]})]
(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
repo
issue-number
transaction-hash)
(: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))
(issues/update-transaction-hash issue-id transaction-hash))
(eth/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)))))

View File

@ -40,26 +40,12 @@
(jdbc/with-db-connection [con-db *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
[issue-id login]
(jdbc/with-db-connection [con-db *db*]
(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
[]
(jdbc/with-db-connection [con-db *db*]
@ -101,11 +87,4 @@
(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})))

View File

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

@ -1,15 +1,16 @@
(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]]
[clojure.string :refer [join]]
[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]
[mount.core :as mount]
[clj-time.core :as t]
[commiteth.util.util :refer [json-api-request]])
(:import [org.web3j
protocol.Web3j
@ -52,37 +53,78 @@
(throw (ex-info "Make sure you provided proper credentials in appropriate resources/config.edn"
{:password password :file-path file-path}))))))
(defrecord TxNonce [tx-hash nonce type timestamp])
(def nonce-being-mined (atom nil))
(defn get-nonce []
(let [nonce (.. (.ethGetTransactionCount @web3j-obj
(env :eth-account)
DefaultBlockParameterName/PENDING)
sendAsync
get
getTransactionCount)]
(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))))))
(.. (.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]

View File

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

View File

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