Class: BTC::AssetProcessor
- Inherits:
-
Object
- Object
- BTC::AssetProcessor
- Defined in:
- lib/btcruby/open_assets/asset_processor.rb
Overview
AssetProcessor implements verification and discovery of assets. This assumes that underlying Bitcoin transactions are valid and not double-spent. Only OpenAssets validation is applied to determine which outputs are holding which assets and how much.
Instance Attribute Summary collapse
-
#network ⇒ Object
Network to use for encoding AssetIDs.
-
#source ⇒ Object
AssetProcessorSource instance that provides transactions.
Instance Method Summary collapse
- #color_transaction_inputs(atx) ⇒ Object
-
#discover_asset(asset_id: nil) ⇒ Object
scans forward and validates every AssetTransaction on the way.
-
#initialize(source: nil, network: nil) ⇒ AssetProcessor
constructor
A new instance of AssetProcessor.
-
#partial_verify_asset_transaction(asset_transaction) ⇒ Object
Returns a list of asset transactions remaining to verify.
-
#verify_asset_transaction(asset_transaction) ⇒ Object
Scans backwards and validates every AssetTransaction on the way.
-
#verify_issues(asset_transaction) ⇒ Object
Attempts to verify issues.
-
#verify_transfers(asset_transaction) ⇒ Object
Attempts to verify transfer transactions assuming all inputs are verified.
Constructor Details
#initialize(source: nil, network: nil) ⇒ AssetProcessor
Returns a new instance of AssetProcessor.
16 17 18 19 20 |
# File 'lib/btcruby/open_assets/asset_processor.rb', line 16 def initialize(source: nil, network: nil) raise ArgumentError, "Source is missing." if !source @source = source @network = network || Network.default end |
Instance Attribute Details
#network ⇒ Object
Network to use for encoding AssetIDs. Default is Network.default.
14 15 16 |
# File 'lib/btcruby/open_assets/asset_processor.rb', line 14 def network @network end |
#source ⇒ Object
AssetProcessorSource instance that provides transactions.
11 12 13 |
# File 'lib/btcruby/open_assets/asset_processor.rb', line 11 def source @source end |
Instance Method Details
#color_transaction_inputs(atx) ⇒ Object
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/btcruby/open_assets/asset_processor.rb', line 93 def color_transaction_inputs(atx) atx.inputs.each do |ain| if !ain.verified? prev_atx = ain.previous_asset_transaction BTC::Invariant(!!prev_atx, "Must have previous asset transaction") if !prev_atx.verified? #puts "\n Prev ATX not fully verified: #{prev_atx.inspect} -> input #{ain.index} of #{atx.inspect}" end BTC::Invariant(prev_atx.verified?, "Must have previous asset transaction outputs fully verified") output = prev_atx.outputs[ain.transaction_input.previous_index] BTC::Invariant(output && output.verified?, "Must have a valid reference to a previous verified output") # Copy over color information. The output can be colored or uncolored. ain.asset_id = output.asset_id ain.value = output.value ain.verified = true end end end |
#discover_asset(asset_id: nil) ⇒ Object
scans forward and validates every AssetTransaction on the way.
308 309 310 |
# File 'lib/btcruby/open_assets/asset_processor.rb', line 308 def discover_asset(asset_id: nil) # TODO: ... end |
#partial_verify_asset_transaction(asset_transaction) ⇒ Object
Returns a list of asset transactions remaining to verify.
Returns an empty array if verification succeeded and there is nothing more to verify.
Returns nil if verification failed.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/btcruby/open_assets/asset_processor.rb', line 116 def partial_verify_asset_transaction(asset_transaction) raise ArgumentError, "Asset Transaction is missing" if !asset_transaction # 1. Verify issuing transactions and collect transfer outputs cache_transactions do if !verify_issues(asset_transaction) return nil end # No transfer outputs, this transaction is verified. # If there are assets on some inputs, they are destroyed. if asset_transaction.transfer_outputs.size == 0 # We keep inputs unverified to indicate that they were not even processed. return [] end # 2. Fetch parent transactions to verify. # * Verify inputs from non-OpenAsset transactions. # * Return OA transactions for verification. # * Link each input to its OA transaction output. prev_unverified_atxs_by_hash = {} asset_transaction.inputs.each do |ain| txin = ain.transaction_input # Ask source if it has a cached verified transaction for this input. prev_atx = @source.verified_asset_transaction_for_hash(txin.previous_hash) if prev_atx BTC::Invariant(prev_atx.verified?, "Cached verified tx must be fully verified") end prev_atx ||= prev_unverified_atxs_by_hash[txin.previous_hash] if !prev_atx prev_tx = transaction_for_input(txin) if !prev_tx Diagnostics.current.("Failed to load previous transaction for input #{ain.index}: #{txin.previous_id}") return nil end begin prev_atx = AssetTransaction.new(transaction: prev_tx) prev_unverified_atxs_by_hash[prev_atx.transaction_hash] = prev_atx rescue FormatError => e # Previous transaction is not a valid Open Assets transaction, # so we mark the input as uncolored and verified as such. ain.asset_id = nil ain.value = nil ain.verified = true end end # Remember a reference to this transaction so we can validate the whole `asset_transaction` when all previous ones are set and verified. ain.previous_asset_transaction = prev_atx end # each input # Return all unverified transactions. # Note: this won't include the already verified one. prev_unverified_atxs_by_hash.values end end |
#verify_asset_transaction(asset_transaction) ⇒ Object
Scans backwards and validates every AssetTransaction on the way.
Does not verify Bitcoin transactions (assumes amounts and scripts are already validated).
Updates verified flags on the asset transaction.
Returns true if asset transaction is verified succesfully.
Returns false otherwise.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/btcruby/open_assets/asset_processor.rb', line 27 def verify_asset_transaction(asset_transaction) raise ArgumentError, "Asset Transaction is missing" if !asset_transaction # Perform a depth-first scanning. # When completed, we'll only have transactions that have all previous txs fully verified. atx_stack = [ asset_transaction ] i = 0 max_size = atx_stack.size while i < atx_stack.size # note: array dynamically changes as we scan it atx = atx_stack[i] BTC::Invariant(atx.is_a?(AssetTransaction), "Must be AssetTransaction instance") more_atxs_to_verify = partial_verify_asset_transaction(atx) if more_atxs_to_verify == nil # Validation failed - return immediately return false elsif more_atxs_to_verify.size == 0 # atx is fully verifiable (issue-only or we used cached parents), remove it from the list # outputs may not be all verified because they can be transfers with cached verified inputs. # so we need to verify local balance and apply asset ids from inputs to outputs if !verify_transfers(atx) return false end if i == 0 # that was the topmost transaction, we are done. return true end @source.asset_transaction_was_verified(atx) atx_stack.delete_at(i) i -= 1 # If this is was the last parent to check, then the previous item would be :parents marker # Once we validate the child behind that marker, we might have another child or the marker again. # Unroll the stack until the previous item is not a child with all parents verified. while (child_atx = atx_stack[i]) == :parents BTC::Invariant(i >= 1, ":parents marker should be preceded by an asset transaction") atx_stack.delete_at(i) i -= 1 child_atx = atx_stack.delete_at(i) # Now all inputs are verified, we only need to verify the transfer outputs against them. # This will make outputs verified for the later transactions (earlier in the list). if !verify_transfers(child_atx) return false end # Now transaction is fully verified. # Source can cache it if needed. @source.asset_transaction_was_verified(child_atx) if i == 0 # this was the topmost child, return return true end i -= 1 end else # we have more things to verify - dig inside these transactions # Start with the last one so once any tx is verifed, we can move back and color inputs of the child transaction. atx_stack.insert(i+1, :parents, *more_atxs_to_verify) max_size = atx_stack.size if atx_stack.size > max_size j = i i += more_atxs_to_verify.size + 1 end end return true end |
#verify_issues(asset_transaction) ⇒ Object
Attempts to verify issues. Fetches parent transactions to determine AssetID.
Returns true if verified all issue outputs.
Returns false if previous tx defining AssetID is not found.
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/btcruby/open_assets/asset_processor.rb', line 175 def verify_issues(asset_transaction) previous_txout = nil # fetch only when we have > 0 issue outputs asset_transaction.outputs.each do |aout| if !aout.verified? if aout.value && aout.value > 0 if aout.issue? previous_txout ||= transaction_output_for_input(asset_transaction.inputs[0].transaction_input) if !previous_txout Diagnostics.current.("Failed to assign AssetID to issue output #{aout.index}: can't find output for input #0") return false end aout.asset_id = AssetID.new(script: previous_txout.script, network: self.network) # Output issues some known asset and amount and therefore it is verified. aout.verified = true else # Transfer outputs must be matched with known asset ids on the inputs. end else # Output without a value is uncolored. aout.asset_id = nil aout.value = nil aout.verified = true end end end true end |
#verify_transfers(asset_transaction) ⇒ Object
Attempts to verify transfer transactions assuming all inputs are verified.
Returns true if all transfers are verified (also updates verified and asset_id on them).
Returns false if any transfer is invalid or some inputs are not verified.
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/btcruby/open_assets/asset_processor.rb', line 206 def verify_transfers(asset_transaction) # Do not verify colors on inputs if no transfers occur. # Typically it's an issuance tx. If there are assets on inputs, they are destroyed. if asset_transaction.transfer_outputs.size == 0 return true end color_transaction_inputs(asset_transaction) current_asset_id = nil inputs = asset_transaction.inputs.dup current_asset_id = nil current_input = nil current_input_remainder = 0 asset_transaction.outputs.each do |aout| # Only check outputs that can be colored (value > 0) and are transfer outputs (after the marker) if aout.has_value? && !aout.marker? && !aout.issue? aout.asset_id = nil remaining_value = aout.value # Try to fill in the output with available inputs. while remaining_value > 0 BTC::Invariant((current_input_remainder == 0) ? (current_input == nil) : true, "Remainder must be == 0 only when current_input is nil") BTC::Invariant((current_input_remainder > 0) ? (current_input && current_input.colored?) : true, "Remainder must be > 0 only when transfer input is colored") current_input ||= inputs.shift # skip uncolored inputs while current_input && !current_input.colored? current_input = inputs.shift end if !current_input Diagnostics.current.("Failed to assign AssetID to transfer output #{aout.index}: not enough colored inputs (#{remaining_value} missing for output #{aout.index}).") return false end # Need to consume aout.value units from inputs and extract the asset ID if !current_input.verified? Diagnostics.current.("Failed to assign AssetID to transfer output #{aout.index}: input #{current_input.index} is not verified.") return false end # Make sure asset ID matches. # If output gets assigned units from 2 or more inputs, all asset ids must be the same. if !aout.asset_id aout.asset_id = current_input.asset_id else if aout.asset_id != current_input.asset_id Diagnostics.current.("Failed to assign AssetID to transfer output #{aout.index}: already assigned another AssetID.") return false end end # If we have remainder from the previous output, use it. # Otherwise use the whole input's value. qty = if current_input_remainder > 0 current_input_remainder else current_input.value end if qty <= remaining_value remaining_value -= qty # choose next input, clear remainder. current_input_remainder = 0 current_input = nil else current_input_remainder = qty - remaining_value remaining_value = 0 # keep the current input to use with `current_input_remainder` in the next output end end # filling in the output aout.verified = true BTC::Invariant(aout.verified && aout.asset_id && aout.value > 0, "Transfer output should be fully verified") end # only transfer outputs with value > 0 end # each output # Some inputs may have remained. If those have some assets, they'll be destroyed. if current_input_remainder > 0 Diagnostics.current.("Warning: #{current_input_remainder} units left over from input #{current_input.index} will be destroyed.") end while current_input = inputs.shift if current_input.colored? Diagnostics.current.("Warning: #{current_input.value} units from input #{current_input.index} will be destroyed.") end end return true end |