fusion.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /* Import modules. */
  2. import { defineStore } from 'pinia'
  3. // used for tagging fusions in a way privately derived from wallet name
  4. const tag_seed = secrets.token_bytes(16)
  5. // Safety limits to prevent loss of funds / limit fees:
  6. // (Note that if we enter multiply into the same fusion, our limits apply
  7. // separately for each "player".)
  8. // Deny server that asks for more than this component feerate (sat/kbyte).
  9. const MAX_COMPONENT_FEERATE = 5000
  10. // The largest 'excess fee' that we are willing to pay in a fusion (fees beyond
  11. // those needed to pay for our components' inclusion)
  12. const MAX_EXCESS_FEE = 10000
  13. // Even if the server allows more, put at most this many inputs+outputs+blanks
  14. const MAX_COMPONENTS = 40
  15. // The largest total fee we are willing to pay (our contribution to transaction
  16. // size should not exceed 7 kB even with 40 largest components).
  17. const MAX_FEE = MAX_COMPONENT_FEERATE * 7 + MAX_EXCESS_FEE
  18. // For privacy reasons, don't submit less than this many distinct tx components.
  19. // (distinct tx inputs, and tx outputs)
  20. const MIN_TX_COMPONENTS = 11
  21. const TOR_PORTS = [9050, 9150]
  22. // # if more than <N> tor connections have been made recently (see covert.py) then don't start auto-fuses.
  23. const AUTOFUSE_RECENT_TOR_LIMIT_LOWER = 60
  24. // # if more than <N> tor connections have been made recently (see covert.py) then shut down auto-fuses that aren't yet started
  25. const AUTOFUSE_RECENT_TOR_LIMIT_UPPER = 120
  26. // # heuristic factor: guess that expected number of coins in wallet in equilibrium is = (this number) / fraction
  27. const COIN_FRACTION_FUDGE_FACTOR = 10
  28. // # for semi-linked addresses (that share txids in their history), allow linking them with this probability:
  29. const KEEP_LINKED_PROBABILITY = 0.1
  30. // # how long an auto-fusion may stay in 'waiting' state (without starting-soon) before it cancels itself
  31. const AUTOFUSE_INACTIVE_TIMEOUT = 600
  32. // # how many random coins to select max in 1 batch -- used by select_random_coins
  33. const DEFAULT_MAX_COINS = 20
  34. // assert DEFAULT_MAX_COINS > 10
  35. // # how many autofusions can be running per-wallet
  36. const MAX_AUTOFUSIONS_PER_WALLET = 10
  37. const CONSOLIDATE_MAX_OUTPUTS = MIN_TX_COMPONENTS // 3
  38. // # Threshold for proportion of total wallet value fused before stopping fusion. This avoids re-fusion due to dust.
  39. const FUSE_DEPTH_THRESHOLD = 0.999
  40. // # We don't allow a fuse depth beyond this in the wallet UI
  41. const MAX_LIMIT_FUSE_DEPTH = 10
  42. const Autofuse = False
  43. const AutofuseCoinbase = False
  44. const AutofuseConfirmedOnly = False
  45. const CoinbaseSeenLatch = False
  46. const FusionMode = 'normal'
  47. const QueudAutofuse = 4
  48. const FuseDepth = 0 # Fuse forever by default
  49. const Selector = ('fraction', 0.1) # coin selector options
  50. const SelfFusePlayers = 1 # self-fusing control (1 = just self, more than 1 = self fuse up to N times)
  51. const SpendOnlyFusedCoins = False # spendable_coin_filter @hook
  52. const HUSH_OP_DATA_HEX = '48555348' // HUSH (as hex)
  53. const HUSH_DERIVATION_PATH = `m/44'/0'/1213551432'` // HUSH (as decimal)
  54. def size_of_input(pubkey):
  55. # Sizes of inputs after signing:
  56. # 32+8+1+1+[length of sig]+1+[length of pubkey]
  57. # == 141 for compressed pubkeys, 173 for uncompressed.
  58. # (we use schnorr signatures, always)
  59. assert 1 < len(pubkey) < 76 # need to assume regular push opcode
  60. return 108 + len(pubkey)
  61. def size_of_output(scriptpubkey):
  62. # == 34 for P2PKH, 32 for P2SH
  63. assert len(scriptpubkey) < 253 # need to assume 1-byte varint
  64. return 9 + len(scriptpubkey)
  65. def component_fee(size, feerate):
  66. # feerate in sat/kB
  67. # size and feerate should both be integer
  68. # fee is always rounded up
  69. return (size * feerate + 999) // 1000
  70. def dust_limit(lenscriptpubkey):
  71. return 3*(lenscriptpubkey + 148)
  72. /**
  73. * Fusion Store
  74. */
  75. export const useFusionStore = defineStore('fusion', {
  76. state: () => ({
  77. /* Initialize session. */
  78. _session: null,
  79. _apiKeys: {},
  80. }),
  81. getters: {
  82. session(_state) {
  83. return _state._session || null
  84. },
  85. sessionid(_state) {
  86. return _state._session?.id || null
  87. },
  88. challenge(_state) {
  89. return _state._session?.challenge || null
  90. },
  91. apiKey(_state) {
  92. return (_exchangeid) => _state._apiKeys[_exchangeid] || null
  93. },
  94. // db() {
  95. // return Db.fusions
  96. // },
  97. },
  98. actions: {
  99. async initFusion () {
  100. console.log('INIT FUSION (before):', this._session)
  101. /* Check for existing session. */
  102. if (this._session) {
  103. return this._session
  104. }
  105. /* Request new session. */
  106. const session = await $fetch('/api/newFusion')
  107. console.log('INIT FUSION (after fetch):', session)
  108. /* Set session. */
  109. this._setFusion(session)
  110. /* Return session. */
  111. return session
  112. },
  113. async run() {
  114. // # Version check and download server params.
  115. self.greet()
  116. server_connected_and_greeted = True
  117. self.notify_server_status(True)
  118. # In principle we can hook a pause in here -- user can insert coins after seeing server params.
  119. if not self.coins:
  120. raise FusionError('Started with no coins')
  121. self.allocate_outputs()
  122. # In principle we can hook a pause in here -- user can tweak tier_outputs, perhaps cancelling some unwanted tiers.
  123. # Register for tiers, wait for a pool.
  124. self.register_and_wait()
  125. # launch the covert submitter
  126. covert = self.start_covert()
  127. try:
  128. # Pool started. Keep running rounds until fail or complete.
  129. while True:
  130. self.roundcount += 1
  131. if self.run_round(covert):
  132. break
  133. finally:
  134. covert.stop()
  135. }
  136. deleteFusion() {
  137. /* Set session. */
  138. this._setFusion(null)
  139. },
  140. saveFusion(_session) {
  141. /* Set session. */
  142. this._setFusion(_session)
  143. },
  144. /**
  145. * Set Fusion
  146. *
  147. * @param {Object} _session Save session details.
  148. */
  149. _setFusion (_session) {
  150. /* Set session. */
  151. this._session = _session
  152. console.log('SET FUSION', this._session)
  153. },
  154. /**
  155. * Set API Key
  156. *
  157. * @param {Object} _key Information for the Exchange's API key.
  158. */
  159. setApiKey (_key: Object) {
  160. /* Set session. */
  161. this._apiKeys[_key.exchangeid] = _key
  162. console.log('SET API KEY', this._apiKeys)
  163. },
  164. /**
  165. * Make up to `max_number` random output values, chosen using exponential
  166. distribution function. All parameters should be positive `int`s.
  167. None can be returned for expected types of failures, which will often occur
  168. when the input_amount is too small or too large, since it becomes uncommon
  169. to find a random assortment of values that satisfy the desired constraints.
  170. On success, this returns a list of length 1 to max_count, of non-negative
  171. integer values that sum up to exactly input_amount.
  172. The returned values will always exactly sum up to input_amount. This is done
  173. by renormalizing them, which means the actual effective `scale` will vary
  174. depending on random conditions.
  175. If `allow_extra_change` is passed (this is abnormal!) then this may return
  176. max_count+1 outputs; the last output will be the leftover change if all
  177. max_counts outputs were exhausted.
  178. */
  179. random_outputs_for_tier(rng, input_amount, scale, offset, max_count, allow_extra_change=False),
  180. /**
  181. * Generate a full set of fusion components, commitments, keys, and proofs.
  182. count: int
  183. inputs: dict of {(prevout_hash, prevout_n): (pubkey, integer value in sats)}
  184. outputs: list of [(value, addr), (value, addr) ...]
  185. feerate: int (sat/kB)
  186. Returns:
  187. list of InitialCommitment,
  188. list of component original indices (inputs then outputs then blanks),
  189. list of serialized Component,
  190. list of Proof,
  191. list of communication privkey,
  192. Pedersen amount for total, (== excess fee)
  193. Pedersen nonce for total,
  194. */
  195. gen_components(num_blanks, inputs, outputs, feerate),
  196. },
  197. })