Compare commits

...

91 Commits

Author SHA1 Message Date
catbref
6c1b21da22 Bump to v1.3.2 2020-08-12 17:30:37 +01:00
catbref
f6216b9745 Respect repositoryBackupInterval when deciding whether to perform backup during auto-update. Issue #10 2020-08-12 14:22:36 +01:00
catbref
91e82d1e3c Add HSQLDB CHECKPOINT statements between database reshaping statements to reduce failures 2020-08-12 14:20:23 +01:00
catbref
50e2bda020 Quicken blockchain validity check at start-up by only checking most recent 1440 blocks 2020-08-12 14:19:14 +01:00
catbref
ab1de1aafa Improve HSQLDB query for finding latest AT state data 2020-08-12 14:18:05 +01:00
catbref
d4ac87f91d Update to more efficient CIYAM AT v1.3.7 2020-08-12 14:17:09 +01:00
catbref
52f4008725 Bump version to v1.3.1 2020-08-10 15:24:37 +01:00
catbref
d8dd71ff50 Fix off-by-one in some sync cases 2020-08-10 15:22:01 +01:00
catbref
02966bf39a Update CIYAM-AT to v1.3.6 to make use of lambda-based logging 2020-08-10 14:05:42 +01:00
catbref
a83d8bf1d5 Split Synchronizer into two strategies depending on whether swapping chains or simply adding new blocks 2020-08-10 14:00:45 +01:00
catbref
1e4432b1f3 Convert LOBs to VARBINARY in DB, reducing sizes at the same time 2020-08-10 13:59:56 +01:00
catbref
d50c979d9f Tighten limit on AT sizes 2020-08-10 13:59:20 +01:00
catbref
4e60ec5192 Disable peer buffer dealloc for now to reduce GC pressure 2020-08-10 13:58:12 +01:00
catbref
31c4e3b1be Reduce memory PoW during network handshake 2020-08-10 13:57:16 +01:00
catbref
b97fbd3171 Bug-fix for cached online accounts 2020-08-07 16:19:32 +01:00
catbref
43fb5d9332 Cache top 2 blocks' worth of online account data to avoid unnecessary Ed25519 verifications 2020-08-07 13:33:31 +01:00
catbref
ea3f1a8eff Actually respond to peers requesting unknown block instead of letting them timeout on their side 2020-08-07 13:32:19 +01:00
catbref
7bb060781e Bump to v1.3.0 - including new trade-portal feature 2020-08-06 15:40:11 +01:00
catbref
a1ab0b7c31 Added more nodes to initial list 2020-08-06 15:38:52 +01:00
catbref
fae2afd010 Remove obsolete github repo from potential auto-update sites 2020-08-06 15:23:13 +01:00
catbref
76c0a5a4fa Increase default Bitcoin transaction fee to 5000 sats 2020-08-06 13:12:54 +01:00
catbref
cdb65657b6 Added qortal.ru nodes to initial nodes list used when creating DB 2020-08-06 09:51:14 +01:00
catbref
9007dfe779 Added API call POST /crosschain/btc/send for sending Bitcoin 2020-08-06 09:46:08 +01:00
catbref
99d09a9877 Change HTTP response codes for BTC_BALANCE_ISSUE and BTC_TOO_SOON from 422 to 402 & 408 2020-08-06 09:45:38 +01:00
catbref
afcf51399e Minor post-merge fix-up 2020-08-06 09:18:08 +01:00
catbref
47679b7f6c Merge branch 'trade-bot' 2020-08-06 09:00:45 +01:00
catbref
8f2985862d Update BTC-ACCT 'cancel' API call to expect AT creator's as sender 2020-08-06 08:52:51 +01:00
catbref
23a524b464 BTC-ACCT: change AT so 'cancel' MESSAGE needs to come from AT creator's address (not trade address) so fee can be used instead of PoW for faster cancels 2020-08-06 08:23:49 +01:00
catbref
ce8992867d Include last 24 hours of CANCELLED & REFUNDED trade offers in first message 2020-08-05 20:58:25 +01:00
catbref
c89de7adfb Add creatorAddress, qortAmount and (last updated) timestamp to trade-bot entries 2020-08-05 16:00:40 +01:00
catbref
cac68ccc14 Added trade-bot websocket 2020-08-05 13:23:24 +01:00
catbref
d507383487 Rework ApiWebSocket so it can manage sessions and in readiness to conversion from "notifiers" to event-bus 2020-08-05 13:23:07 +01:00
catbref
ce5cf87094 Added unified, simple event bus to eventually replace controller "notifiers" 2020-08-05 13:20:19 +01:00
catbref
ec2c9d2a44 Improve /crosschain/tradebot/respond with varied API errors such as BTC_BALANCE_ISSUE, BTC_NETWORK_ISSUE, etc. instead of just "false" 2020-08-05 10:05:09 +01:00
catbref
36d0abe635 WIP: trade-bot: log warning when we can't fund P2SH-B for some reason 2020-08-05 10:04:25 +01:00
catbref
615381ca5a Fix BTC spend txn building to be less aggressive about caching/checking spent keys 2020-08-05 10:03:08 +01:00
catbref
6b83499216 WIP: trade-bot: add support for showing trade partner's Qortal receiving address in trade offer summaries 2020-08-04 20:35:22 +01:00
catbref
faa2e9502b WIP: trade-bot: include creation/latest timestamp (as appropriate) in trade offer summaries via websocket 2020-08-04 17:00:36 +01:00
catbref
cd07240ce7 Add BTC.getWalletBalance(xprv) and add API call to access that.
Also improved BTC.WalletAwareUTXOProvider to derive more keys itself
instead of throwing and relying on caller to do the work.
Added benefit of cleaning up caller code and being more efficient.
Needed because not all receiving/change addresses were being picked up.
2020-08-04 16:37:44 +01:00
catbref
91518464c2 WIP: trade-bot: fix empty bitcoin wallet edge case when finding UTXOs 2020-08-04 12:26:09 +01:00
catbref
25bf315e23 WIP: trade-bot: tradeoffers websocket initial message with OFFERING/REDEEMED and fixed subsequent messages 2020-08-04 11:21:57 +01:00
catbref
a8743b1bd3 ElectrumX network main-net servers 2020-08-03 19:28:55 +01:00
catbref
f90bd6ee45 WIP: trade-bot: added WS for streaming existing/new trades in OFFERING state 2020-08-03 17:57:22 +01:00
catbref
a351756883 WIP: trade-bot: add missing JavaTypeAdapter to TradeBotData.bitcoinAmount
Also: unify "receiveAddress" to "receivingAddress"

Fix missed changes from "recipient" to "partner" in BTCACCT, etc.
2020-08-03 15:52:17 +01:00
catbref
ea9b0d4588 WIP: trade-bot: initial API call for listing completed trades
Also: renamed trade bot field/column "receiving_public_key_hash"
to "receiving_account_info" as Alice's trade bot uses it to
store Alice's Qortal address, not PKH.

Added some extra simplistic repository calls to support above,
like BlockRepository.getTimestampFromHeight,
ATRepository.getCreatorPublicKey(atAddress)
2020-08-03 14:54:45 +01:00
catbref
e9c85c946e WIP: trade-bot: two more unit tests to cover some edge cases 2020-08-03 10:49:47 +01:00
catbref
876bfb525b WIP: trade-bot: more receive address support, some terminology clarification
Bitcoin receive address no longer stored in AT but dealt with by trade-bot.
This allows 'Bob' to have his BTC sent anywhere he likes when redeeming P2SH-A
thus saving a step, typically incurred by UI. DB shape change due to this.

Similarly, AT code has been updated to expect a Qortal receiving address when
Alice sends MESSAGE to redeem AT.

This means both trade-bot entries (Alice/Bob) can be safely wiped once trade completes.

Some terms were confusing like "trade recipient" which actually referred to
Alice and so have been unified as "trade partner" as to not be confused with
(say) "recipient address"

The MESSAGEs sent from Alice to Bob, from Bob to AT and from Alice to AT have been
given more useful names: 'offer', 'trade' and 'redeem'. There is also a cancel
MESSAGE sent from Bob to AT to cancel AT before trading occurs.

Some API calls have been renamed in light of above.

AT's 'mode' has been expanded from simply OFFER/TRADE to:
OFFERING, TRADING, REFUNDED, REDEEMED, CANCELLED

Tests updated, but MORE TESTING REQUIRED BEFORE RELEASE
2020-08-03 09:36:46 +01:00
catbref
6be67d0d92 WIP: trade-bot: make sure the "trade" private key is valid for both Curve25519 and secp256k1 2020-07-30 08:12:45 +01:00
catbref
16581766c6 WIP: trade-bot: detect and remove mempool entries from ElectrumX "listunspent" results 2020-07-29 20:48:06 +01:00
catbref
7fd7104f46 WIP: trade-bot: add flag to be set by AT if redeem happens so trade-bot detects redeem instead of refund 2020-07-29 20:25:28 +01:00
catbref
d2cae7c8b5 WIP: trade-bot: use correct Bob Bitcoin receive address in log entry 2020-07-29 18:14:47 +01:00
catbref
83955acd22 WIP: trade-bot: allow trade-bot entries to be deleted if in BOB_WAITING_FOR_AT_CONFIRM state. Also, return false (instead of throwing internal error) if trade-bot entry does not exist 2020-07-29 18:13:27 +01:00
catbref
d85b746021 WIP: trade-bot: add xprv validation method to BTC class and use that for API call /crosschain/tradebot/respond instead of vague byte-length check 2020-07-29 18:11:47 +01:00
catbref
e2dc91c1ea Fix API call DELETE /crosschain/tradeoffer regarding PoW MESSAGE reference 2020-07-29 10:38:32 +01:00
catbref
098e2623d6 WIP: cross-chain AT now stores bitcoin receiving PKH 2020-07-28 17:21:54 +01:00
catbref
2df045396d Bump to v1.2.3 2020-07-28 11:27:21 +01:00
catbref
6c182a3567 Allow minting accounts to be removed from node using public key as well as private key 2020-07-28 10:45:06 +01:00
catbref
340d6dfc8d Add websocket error handler support 2020-07-27 10:16:21 +01:00
catbref
eb27b0d3e2 Blocks websocket now returns simpler block info 2020-07-24 14:52:14 +01:00
catbref
7377893050 WebSocket improvements, inc. bump Jetty to v9.4.29-20200521
Various issues in Jetty v9.4.22 (and some later versions too)
cause websockets to use up all available threads.

Bumped Jetty to v9.4.29 to resolve some of these issues.

Changed some Qortal-side websocket code to minimize
locking on websocket notifiers. Websocket messages now
sent async, although the returned Futures are discarded,
as it's up to the remote end to consume fast enough.

Changed Controller to only request a SysTray update before
synchronization if there's a chance node might change height.
Similarly, Controller only requests SysTray update after
synchronization if chain tip has actually changed.
Both of the above together should reduce the number of
messages sent out via the admin status websockets.
2020-07-24 10:34:42 +01:00
catbref
21d7a4eed1 Improved AT PUT_TX_AFTER_TIMESTAMP_INTO_A function
Previous version fetched all the blocks from previous 'timestamp'
to current height, checking each transaction. (very slow)

New implementation leverages repository to do the heavy lifting.

Could potentially benefit from some DB indexes in the future?

Added unit test to cover.
2020-07-23 08:43:27 +01:00
catbref
fb2c2b1d09 Added API call GET /blocks/summaries
Returns summary info about a range of blocks.

(Not to be confused with network-related BlockSummaries)
2020-07-20 13:05:43 +01:00
catbref
6f2dd6c8d0 Added some more useful tools/scripts, mostly for Linux-based curious node owners 2020-07-17 12:22:48 +01:00
catbref
4cc0e7845f Add instructions and files used to build installers for Windows
For this commit, the included .aip file, and qortal.jar, match
what was used to produce the installer for release v1.2.2.

In a future commit, maybe remove qortal.jar as it is only included
here to illustrate current location in build tree.

Updates to .aip file could be, and maybe even should be, committed.

This build toolchain uses AdvancedInstaller v16 or better but
may require an (expensive) enterprise licence. It is possible
to obtain an 'open source'-use free licence from AdvancedInstaller
by contacting them directly. However this may result in restricted
functionality with AdvancedInstaller and some installer features,
e.g. multi-language support, may have be to removed.
2020-07-17 12:08:25 +01:00
catbref
ca8eabc425 Commit 2 of 2: adding new CIYAM AT v.1.3.5 2020-07-17 11:46:39 +01:00
catbref
94c83d6a93 Commit 1 of 2: removing old CIYAM AT v1.3.4 2020-07-17 11:46:21 +01:00
catbref
dea2f34c52 Trade-bot: more comments, more documentation, more ElectrumX servers.
Bitcoin main-net ElectrumX server list added to ElectrumX class,
albeit commented out at this point until it is decided that trade-bot
is ready for production use. (Simply remove the leading //s)

More comments and documentation has been added to TradeBot class
to further describe the actions taken.

It is important to note that:

Bitcoin wallet access is required by trade-bot

and so:

A Bitcoin WALLET PRIVATE KEY is stored in the database by trade-bot

and hence, if you use trade-bot:

DO NOT DISTRIBUTE YOUR DB FILES TO ANYONE ELSE!

Furthermore it should be obvious that this functionality is provided on
a 'best effort", not guaranteed, basis, therefore:

YOUR FUNDS ARE AT RISK!

If you are unsure about any aspect, or cannot afford to lose your funds,
or it's possible that unexpected outcomes occur, then DO NOT USE.

To use trade-bot on Bitcoin TESTNET then this to your settings JSON file:
  "bitcoinNet": "TEST3",

See Settings.java line 100, and BTC class for more info.
2020-07-17 11:28:22 +01:00
catbref
b294f5e333 WIP: More defensive ElectrumX calls. Bring non-trade-bot API calls up to date 2020-07-17 08:39:45 +01:00
catbref
f9b726a75d WIP: TradeBot - added refunding code 2020-07-17 08:39:45 +01:00
catbref
579645d6b7 WIP: trade-bot now does complete end-to-end trade (more work needed)
bitcoinj now uses ElectrumX as an UTXO provider in order to keep track
of coins in BIP32 deterministic wallet.

Trade responder (Alice) needs to pass a BIP32 extended private key to API
so trade-bot can create unattended spends.

Both Alice and Bob can find their final funds in accounts using the
ephemeral 'tradePrivateKey' from trade-bot state data.

Most cross-chain API calls are now only allowed from localhost.

Most Bitcoin fees pegged at 0.00001000 BTC.

More work needed to handle refunds in case of trade failures.
(See XXX comment tags in TradeBot.java)
2020-07-17 08:39:45 +01:00
catbref
e729571a21 WIP: trade-bot: do not run trade-bot if not up-to-date 2020-07-17 08:39:45 +01:00
catbref
f179139967 WIP: trade-bot: Alice P2SH_a progress
Qortal AT now includes suggested tradeTimeout again as a constant so trade partner/recipient can use that to calculate a suitable lockTimeA. CODE_HASH changed!

Renamed some secret_hash to hash_of_secret.

Changed TradeBotStates.trade_state back to TINYINT and adjusted values in TradeBotData.State enum to suit.
Added lockTimeA to TradeBotData & repository.

Added JAXB-only extra representations of Bitcoin PKHs as addresses.

Fixed incorrect expected length in BTCACCT.extractOfferMessageData().

CrossChainTradeData.refundTimeout now only present in TRADE mode.

Added BTC.pkhToAddress().

Added initial TradeBot.handleAliceWaitingForP2shA().

Enforce only one TradeBot thread running using 'activeFlag' atomic boolean.

Replace incorrect SHA256 with HASH160 for hashOfSecretA in TradeBot.startResponse().
2020-07-17 08:39:45 +01:00
catbref
ee5119e4dd WIP: trade-bot. move trade-bot hook, fix bugs, etc.
Controller now calls TradeBot.onChainTipChange() inside thread
started by Controller.onNewBlock(), instead of blocking
Controller.setChainTip().

DB TradeBotStates has trade_foreign_public_key changed to VARBINARY(33)
as Bitcoin pubkeys aren't uniformly 32 bytes!
Also, trade_state changed from TINYINT to SMALLINT to cover enum value range.

TradeBot.createTrade() incorrectly used Crypto.digest() to create hash-of-secret
instead of Crypto.hash160(). Also corrected tradeState to
BOB_WAITING_FOR_AT_CONFIRM. Also added missing fee calculation.

Added missing repository.saveChanges() to TradeBot methods.

Added balance check to API POST /crosschain/tradebot before passing
request to TradeBot.createTrade(), which also ensures there's a
usable account last-reference too.
2020-07-17 08:39:45 +01:00
catbref
11bf5ac6fc WIP: remove trade_timeout from DB TradeBotStates & TradeBotCreateRequest 2020-07-17 08:39:45 +01:00
catbref
c3eb385066 WIP: cross-chain trading with new lockTimes, requires AT v1.3.5 2020-07-17 08:39:45 +01:00
catbref
886c9156a5 WIP: cross-chain trading AT passes AtTests now 2020-07-17 08:39:45 +01:00
catbref
23062c59cd WIP: trade-bot, particularly the new two-P2SH Qortal AT code 2020-07-17 08:39:45 +01:00
catbref
da254058c5 WIP: split P2SH from BTCACCT, add more fields to TradeBotData, remove initial QORT payout 2020-07-17 08:39:45 +01:00
catbref
a6fa4fc613 WIP: trade-bot MESSAGE support 2020-07-17 08:39:45 +01:00
catbref
593b61ea4b Reduce bitcoinj exposure to classes outside of org.qortal.crosschain package.
BTC.getBalance() now returns Long instead of Coin.

BTC.FORMAT.format(Coin) changed to BTC.format(Coin or long).

Added BTC.deriveP2shAddress(byte[] redeemScriptBytes).
2020-07-17 08:39:45 +01:00
catbref
04d691991a WIP: more work on trade-bot 2020-07-17 08:39:45 +01:00
catbref
faa6e82bef WIP on trade-bot 2020-07-17 08:39:45 +01:00
catbref
65ccb80aa4 AT-related changes: new Qortal functions, tests, etc.
Added GET_MESSAGE_LENGTH_FROM_TX_IN_A
and PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.

Replaced AT-1.3.4 with version including bug-fix for off-by-one
data address bounds checking.

Moved long-from-bytes method to BitTwiddling class.

Renamed some methods to make it more obvious they work with
little/big endian data.
2020-07-17 08:39:45 +01:00
catbref
cc13d1d0f1 WIP commit 2020-07-17 08:39:45 +01:00
catbref
ead84d70d1 Initial skeleton code for Trade Bot 2020-07-17 08:39:45 +01:00
catbref
275146fb55 Return REWARD_SHARE_UNKNOWN when trying to cancel non-existent reward-share 2020-07-14 09:53:14 +01:00
catbref
d81729d9f7 Bump to v1.2.2 2020-07-03 12:14:31 +01:00
catbref
e74a249388 Collate network PoW computes into a fixed-sized pool, with dead peer detection.
Also added Named/DaemonThreadFactory classes.

Network EPC now uses NamedThreadFactory for easier debugging.

Added settings field "networkPoWComputePoolSize", default 2, which
seems to work with both low-power ARM boards and high-power desktops.
2020-07-03 09:31:46 +01:00
catbref
d8c5e557d8 Update uiLocalServers, autoUpdateRepos and bump to v1.2.1 2020-06-30 15:41:53 +01:00
catbref
984e8b5227 Network optimizations: if we're not up to date then don't request, or send, unconfirmed transaction lists 2020-06-30 14:26:12 +01:00
catbref
469bf2a63e Improve inbound peer handshaking
If a node accepts a connection from an inbound peer
then remote peer will send RESPONSE first
and local node would previously change handshaking state
to COMPLETED while computing their own RESPONSE.

This meant that the local node would sometimes also start
sending post-handshake messages to the remote peer,
e.g. TRANSACTION_SIGNATURES.

Remote peer is only expecting a RESPONSE message, so would
close connection.

So we introduce an extra handshaking state "RESPONDING" for use
by local node while they compute RESPONSE in a separate thread.
Once the RESPONSE has been sent, local node moves to COMPLETED
state and called onHandshakeCompleted() as per usual.

Note that the code path when connecting outbound to a remote peer
is not changed, and the RESPONDING state is not used.

Also in this commit:

Network.onPeerReady now bypasses call to onMessage and instead
calls onHandshakingMessage() directly to avoid race condition
where peer's handshake status could change between
onPeerReady's caller and onMessage() calling peer.getHandshakeStatus()
2020-06-30 13:37:14 +01:00
98 changed files with 8438 additions and 1181 deletions

View File

@@ -0,0 +1,3 @@
{
"apiDocumentationEnabled": true
}

View File

@@ -0,0 +1,70 @@
rootLogger.level = info
# On Windows, uncomment next line to set dirname:
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
rootLogger.appenderRef.console.ref = stdout
rootLogger.appenderRef.rolling.ref = FILE
# Suppress extraneous bitcoinj library output
logger.bitcoinj.name = org.bitcoinj
logger.bitcoinj.level = error
# Override HSQLDB logging level to "warn" as too much is logged at "info"
logger.hsqldb.name = hsqldb.db
logger.hsqldb.level = warn
# Support optional, per-session HSQLDB debugging
logger.hsqldbRepository.name = org.qortal.repository.hsqldb
logger.hsqldbRepository.level = debug
# Suppress extraneous Jersey warning
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
logger.jerseyInject.level = off
# Suppress extraneous Jersey EOF 'errors' (actually remote peers disconnecting early)
logger.jerseyEOF.name = org.glassfish.jersey.server.internal
logger.jerseyEOF.level = off
# Suppress extraneous Jetty entries
# 2019-02-14 11:46:27 INFO ContextHandler:851 - Started o.e.j.s.ServletContextHandler@6949e948{/,null,AVAILABLE}
# 2019-02-14 11:46:27 INFO AbstractConnector:289 - Started ServerConnector@50ad322b{HTTP/1.1,[http/1.1]}{0.0.0.0:9085}
# 2019-02-14 11:46:27 INFO Server:374 - jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 1.8.0_181-b13
# 2019-02-14 11:46:27 INFO Server:411 - Started @2539ms
logger.jetty.name = org.eclipse.jetty
logger.jetty.level = warn
# Even more extraneous Jetty output
# 2019-01-26 02:18:10 WARN ResourceService:718 - java.util.concurrent.TimeoutException: Idle timeout expired: 30000/30000 ms
logger.jettyRS.name = org.eclipse.jetty.server.ResourceService
logger.jettyRS.level = error
# Suppress extraneous slf4j entries
# 2019-02-14 11:46:27 INFO log:193 - Logging initialized @1636ms to org.eclipse.jetty.util.log.Slf4jLog
logger.slf4j.name = org.slf4j
logger.slf4j.level = warn
# Suppress extraneous Reflections entry
# 2019-02-27 10:45:25 WARN Reflections:179 - given scan urls are empty. set urls in the configuration
logger.orgReflections.name = org.reflections.Reflections
logger.orgReflections.level = off
logger.sunReflections.name = sun.reflect.Reflection
logger.sunReflections.level = off
appender.console.type = Console
appender.console.name = stdout
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = error
appender.rolling.type = RollingFile
appender.rolling.name = FILE
appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.rolling.filePattern = ${dirname:-}${filename}.%i
appender.rolling.policy.type = SizeBasedTriggeringPolicy
appender.rolling.policy.size = 4MB
# Set the immediate flush to true (default)
# appender.rolling.immediateFlush = true
# Set the append to true (default), should not overwrite
# appender.rolling.append=true

View File

@@ -0,0 +1,33 @@
@echo off
:: BatchGotAdmin
:-------------------------------------
REM --> Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
REM --> If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
echo Requesting administrative privileges...
goto UACPrompt
) else ( goto gotAdmin )
:UACPrompt
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"
"%temp%\getadmin.vbs"
exit /B
:gotAdmin
if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
pushd "%CD%"
CD /D "%~dp0"
:--------------------------------------
net stop "Windows Time"
w32tm /config "/manualpeerlist:pool.ntp.org 0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org cn.pool.ntp.org 0.cn.pool.ntp.org 1.cn.pool.ntp.org 2.cn.pool.ntp.org 3.cn.pool.ntp.org"
net start "Windows Time"
sc config w32time start= auto

Binary file not shown.

1268
WindowsInstaller/Qortal.aip Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
# Windows installer
## Prerequisites
* AdvancedInstaller v16 or better, and enterprise licence if translations are required
* Installed AdoptOpenJDK v11 64bit, full JDK *not* JRE
## General build instructions
If this is your first time opening the `qortal.aip` file then you might need to adjust
configured paths, or create a dummy `D:` drive with the expected layout.
Typical build procedure:
* Overwrite the `qortal.jar` file in `Install-Files\`
* Open AdvancedInstaller with qortal.aip file
* If releasing a new version, change version number in:
+ "Product Information" side menu
+ "Product Details" side menu entry
+ "Product Details" tab in "Product Details" pane
+ "Product Version" entry box
* Click away to a different side menu entry, e.g. "Resources" -> "Files and Folders"
* You should be prompted whether to generate a new product key, click "Generate New"
* Click "Build" button
* New EXE should be generated in `Qortal-SetupFiles\` folder with correct version number

97
WindowsInstaller/dictionary.ail Executable file
View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<DICTIONARY type="multilanguage">
<!-- Control table -->
<ENTRY id="Control.Text.CustomizeDataPathDlg#Description">
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
<STRING lang="zh_TW" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDataPathDlg#Text">
<STRING lang="en" value="Select one of the options below, then click &quot;Next&quot;."/>
<STRING lang="zh" value="请选择,然后“下一步”"/>
<STRING lang="zh_TW" value="请选择,然后“下一步”"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDataPathDlg#Title">
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh" value="选择数据保存的文件夹?"/>
<STRING lang="zh_TW" value="选择数据保存的文件夹?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDbDlg#Description">
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh_TW" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDbDlg#Title">
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh_TW" value="Choose Custom Data Storage Folder?"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Description">
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh" value="这里是区块链及其它数据存放的文件夹"/>
<STRING lang="zh_TW" value="这里是区块链及其它数据存放的文件夹"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Text">
<STRING lang="en" value="To store data in this folder, click &quot;[Text_Next]&quot;. To store data in a different folder, enter it below or click &quot;Browse&quot;."/>
<STRING lang="zh" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
<STRING lang="zh_TW" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Title">
<STRING lang="en" value="Select Data Storage Folder"/>
<STRING lang="zh" value="请选择文件存储地方"/>
<STRING lang="zh_TW" value="请选择文件存储地方"/>
</ENTRY>
<ENTRY id="Control.Text.DbFolderDlg#Description">
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh_TW" value="This is the folder where the blockchain, and other data, will be stored."/>
</ENTRY>
<ENTRY id="Control.Text.DbFolderDlg#Title">
<STRING lang="en" value="Select Data Storage Folder"/>
<STRING lang="zh" value="请选择文件存储地方"/>
<STRING lang="zh_TW" value="请选择文件存储地方"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Description">
<STRING lang="en" value="Reconfigure Windows for more accurate time?"/>
<STRING lang="zh" value="重新配置Windows以获得更准确的时间"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_1">
<STRING lang="en" value="An accurate Windows clock is required to connect to the [ProductName] network and make transactions."/>
<STRING lang="zh" value="需要准确的Windows时钟才能连接到[ProductName]网络并进行交易。"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_2">
<STRING lang="en" value="Select one of the options below, then click &quot;Next&quot;."/>
<STRING lang="zh" value="请选择,然后“下一步”"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_3">
<STRING lang="en" value="Your computer&apos;s clock needs to be accurate to within 0.5 seconds."/>
<STRING lang="zh" value="您的计算机时钟需要准确到0.5秒内。"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Title">
<STRING lang="en" value="Windows clock accuracy"/>
<STRING lang="zh" value="Windows 时钟精度"/>
</ENTRY>
<ENTRY id="Control.Text.VerifyRemoveDlg#RemoveBlockchainCheckbox">
<STRING lang="en" value="Remove downloaded blockchain and other data"/>
<STRING lang="zh" value="删除您下载的区块链"/>
</ENTRY>
<!-- RadioButton table -->
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#choose">
<STRING lang="en" value="Choose custom data storage folder..."/>
<STRING lang="zh" value="选择特定的文件夹存储"/>
<STRING lang="zh_TW" value="选择特定的文件夹存储"/>
</ENTRY>
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#default">
<STRING lang="en" value="Use default location "/>
<STRING lang="zh" value="使用默认存储地点"/>
<STRING lang="zh_TW" value="使用默认存储地点"/>
</ENTRY>
<ENTRY id="RadioButton.Text.RECONFIG_NTP#1">
<STRING lang="en" value="Yes, configure Windows to use internet time servers (Recommended)"/>
<STRING lang="zh" value="是将Windows配置为使用多个Internet时间服务器 (推荐的)"/>
</ENTRY>
<ENTRY id="RadioButton.Text.RECONFIG_NTP#2">
<STRING lang="en" value="No, I will manage clock accuracy myself"/>
<STRING lang="zh" value="不,我会自己管理时钟精度。"/>
</ENTRY>
</DICTIONARY>

BIN
WindowsInstaller/qortal.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -4,6 +4,6 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<version>1.3.4</version>
<version>1.3.7</version>
<description>POM was created from install:install-file</description>
</project>

View File

@@ -3,10 +3,13 @@
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<versioning>
<release>1.3.4</release>
<release>1.3.7</release>
<versions>
<version>1.3.4</version>
<version>1.3.5</version>
<version>1.3.6</version>
<version>1.3.7</version>
</versions>
<lastUpdated>20200414162728</lastUpdated>
<lastUpdated>20200812131412</lastUpdated>
</versioning>
</metadata>

View File

@@ -3,13 +3,13 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.2.0</version>
<version>1.3.2</version>
<packaging>jar</packaging>
<properties>
<bitcoinj.version>0.15.5</bitcoinj.version>
<bouncycastle.version>1.64</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<ciyam-at.version>1.3.4</ciyam-at.version>
<ciyam-at.version>1.3.7</ciyam-at.version>
<commons-net.version>3.6</commons-net.version>
<commons-text.version>1.8</commons-text.version>
<dagger.version>1.2.2</dagger.version>
@@ -17,7 +17,7 @@
<hsqldb.version>2.5.0-fixed</hsqldb.version>
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
<jersey.version>2.29.1</jersey.version>
<jetty.version>9.4.22.v20191022</jetty.version>
<jetty.version>9.4.29.v20200521</jetty.version>
<log4j.version>2.12.1</log4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>1.7.12</slf4j.version>

View File

@@ -128,8 +128,8 @@ public enum ApiError {
// Bitcoin
BTC_NETWORK_ISSUE(1201, 500),
BTC_BALANCE_ISSUE(1202, 422),
BTC_TOO_SOON(1203, 422);
BTC_BALANCE_ISSUE(1202, 402),
BTC_TOO_SOON(1203, 408);
private static final Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));

View File

@@ -3,6 +3,7 @@ package org.qortal.api;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -17,7 +18,7 @@ public class ApiErrorHandler extends ErrorHandler {
private static final Logger LOGGER = LogManager.getLogger(ApiErrorHandler.class);
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if (Settings.getInstance().isApiLoggingEnabled()) {
String requestURI = request.getRequestURI();

View File

@@ -18,9 +18,9 @@ import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.CustomRequestLog;
import org.eclipse.jetty.server.DetectorConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.OptionalSslConnectionFactory;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.RequestLogWriter;
import org.eclipse.jetty.server.SecureRequestCustomizer;
@@ -43,6 +43,8 @@ import org.qortal.api.websocket.ActiveChatsWebSocket;
import org.qortal.api.websocket.AdminStatusWebSocket;
import org.qortal.api.websocket.BlocksWebSocket;
import org.qortal.api.websocket.ChatMessagesWebSocket;
import org.qortal.api.websocket.TradeBotWebSocket;
import org.qortal.api.websocket.TradeOffersWebSocket;
import org.qortal.settings.Settings;
public class ApiService {
@@ -113,8 +115,7 @@ public class ApiService {
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
new OptionalSslConnectionFactory(sslConnectionFactory, HttpVersion.HTTP_1_1.asString()),
sslConnectionFactory,
new DetectorConnectionFactory(sslConnectionFactory),
httpConnectionFactory);
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());
@@ -197,6 +198,8 @@ public class ApiService {
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
// Start server
this.server.start();

View File

@@ -0,0 +1,25 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class BitcoinSendRequest {
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
public String xprv58;
@Schema(description = "Recipient's Bitcoin address ('legacy' P2PKH only)", example = "1BitcoinEaterAddressDontSendf59kuE")
public String receivingAddress;
@Schema(description = "Amount of BTC to send")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long bitcoinAmount;
public BitcoinSendRequest() {
}
}

View File

@@ -0,0 +1,71 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.block.BlockData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockInfo {
private byte[] signature;
private int height;
private long timestamp;
private int transactionCount;
private String minterAddress;
protected BlockInfo() {
/* For JAXB */
}
public BlockInfo(byte[] signature, int height, long timestamp, int transactionCount, String minterAddress) {
this.signature = signature;
this.height = height;
this.timestamp = timestamp;
this.transactionCount = transactionCount;
this.minterAddress = minterAddress;
}
public BlockInfo(BlockData blockData) {
// Convert BlockData to BlockInfo, using additional data
this.minterAddress = "unknown?";
try (final Repository repository = RepositoryManager.getRepository()) {
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(blockData.getMinterPublicKey());
if (rewardShareData != null)
this.minterAddress = rewardShareData.getMintingAccount();
} catch (DataException e) {
// We'll carry on with placeholder minterAddress then...
}
this.signature = blockData.getSignature();
this.height = blockData.getHeight();
this.timestamp = blockData.getTimestamp();
this.transactionCount = blockData.getTransactionCount();
}
public byte[] getSignature() {
return this.signature;
}
public int getHeight() {
return this.height;
}
public long getTimestamp() {
return this.timestamp;
}
public int getTransactionCount() {
return this.transactionCount;
}
public String getMinterAddress() {
return this.minterAddress;
}
}

View File

@@ -25,6 +25,9 @@ public class CrossChainBitcoinRedeemRequest {
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
public byte[] secret;
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
public byte[] receivingAccountInfo;
public CrossChainBitcoinRedeemRequest() {
}

View File

@@ -12,20 +12,19 @@ public class CrossChainBuildRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "Initial QORT amount paid when trade agreed", example = "0.00100000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long initialQortAmount;
@Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long finalQortAmount;
public long qortAmount;
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fundingQortAmount;
@Schema(description = "HASH160 of creator's Bitcoin public key", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
public byte[] bitcoinPublicKeyHash;
@Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV")
public byte[] secretHash;
public byte[] hashOfSecretB;
@Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)

View File

@@ -8,10 +8,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainCancelRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
@Schema(description = "AT creator's public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "Qortal AT address")
@Schema(description = "Qortal trade AT address")
public String atAddress;
public CrossChainCancelRequest() {

View File

@@ -0,0 +1,86 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.BTCACCT;
import org.qortal.data.crosschain.CrossChainTradeData;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainOfferSummary {
// Properties
@Schema(description = "AT's Qortal address")
public String qortalAtAddress;
@Schema(description = "AT creator's Qortal address")
public String qortalCreator;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long qortAmount;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long btcAmount;
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
private int tradeTimeout;
private BTCACCT.Mode mode;
private long timestamp;
private String partnerQortalReceivingAddress;
protected CrossChainOfferSummary() {
/* For JAXB */
}
public CrossChainOfferSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
this.qortalCreator = crossChainTradeData.qortalCreator;
this.qortAmount = crossChainTradeData.qortAmount;
this.btcAmount = crossChainTradeData.expectedBitcoin;
this.tradeTimeout = crossChainTradeData.tradeTimeout;
this.mode = crossChainTradeData.mode;
this.timestamp = timestamp;
this.partnerQortalReceivingAddress = crossChainTradeData.qortalPartnerReceivingAddress;
}
public String getQortalAtAddress() {
return this.qortalAtAddress;
}
public String getQortalCreator() {
return this.qortalCreator;
}
public long getQortAmount() {
return this.qortAmount;
}
public long getBtcAmount() {
return this.btcAmount;
}
public int getTradeTimeout() {
return this.tradeTimeout;
}
public BTCACCT.Mode getMode() {
return this.mode;
}
public long getTimestamp() {
return this.timestamp;
}
public String getPartnerQortalReceivingAddress() {
return this.partnerQortalReceivingAddress;
}
}

View File

@@ -8,14 +8,20 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainSecretRequest {
@Schema(description = "Public key to match AT's 'recipient'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] recipientPublicKey;
@Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] partnerPublicKey;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
public byte[] secret;
@Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
public byte[] secretA;
@Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
public byte[] secretB;
@Schema(description = "Qortal address for receiving QORT from AT")
public String receivingAddress;
public CrossChainSecretRequest() {
}

View File

@@ -8,14 +8,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "AT creator's 'trade' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] tradePublicKey;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "Qortal address for trade partner/recipient")
public String recipient;
@Schema(description = "Signature of trading partner's 'offer' MESSAGE transaction")
public byte[] messageTransactionSignature;
public CrossChainTradeRequest() {
}

View File

@@ -0,0 +1,43 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.data.crosschain.CrossChainTradeData;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeSummary {
private long tradeTimestamp;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long qortAmount;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long btcAmount;
protected CrossChainTradeSummary() {
/* For JAXB */
}
public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
this.tradeTimestamp = timestamp;
this.qortAmount = crossChainTradeData.qortAmount;
this.btcAmount = crossChainTradeData.expectedBitcoin;
}
public long getTradeTimestamp() {
return this.tradeTimestamp;
}
public long getQortAmount() {
return this.qortAmount;
}
public long getBtcAmount() {
return this.btcAmount;
}
}

View File

@@ -0,0 +1,36 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotCreateRequest {
@Schema(description = "Trade creator's public key", example = "2zR1WFsbM7akHghqSCYKBPk6LDP8aKiQSRS1FrwoLvoB")
public byte[] creatorPublicKey;
@Schema(description = "QORT amount paid out on successful trade", example = "80.40200000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long qortAmount;
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "81")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fundingQortAmount;
@Schema(description = "Bitcoin amount wanted in return", example = "0.00864200")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long bitcoinAmount;
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
public int tradeTimeout;
@Schema(description = "Bitcoin address for receiving", example = "1BitcoinEaterAddressDontSendf59kuE")
public String receivingAddress;
public TradeBotCreateRequest() {
}
}

View File

@@ -0,0 +1,23 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotRespondRequest {
@Schema(description = "Qortal AT address", example = "AH3e3jHEsGHPVQPDiJx4pYqgVi72auxgVy")
public String atAddress;
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
public String xprv58;
@Schema(description = "Qortal address for receiving QORT from AT")
public String receivingAddress;
public TradeBotRespondRequest() {
}
}

View File

@@ -302,13 +302,13 @@ public class AdminResource {
@DELETE
@Path("/mintingaccounts")
@Operation(
summary = "Remove account/reward-share from use by BlockMinter, using private key",
summary = "Remove account/reward-share from use by BlockMinter, using public or private key",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string", example = "private key"
type = "string", example = "public or private key"
)
)
),
@@ -319,13 +319,13 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
public String deleteMintingAccount(String seed58) {
public String deleteMintingAccount(String key58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] seed = Base58.decode(seed58.trim());
byte[] key = Base58.decode(key58.trim());
if (repository.getAccountRepository().delete(seed) == 0)
if (repository.getAccountRepository().delete(key) == 0)
return "false";
repository.saveChanges();

View File

@@ -25,7 +25,6 @@ import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.at.QortalAtLoggerFactory;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.DeployAtTransactionData;
@@ -147,8 +146,7 @@ public class AtResource {
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
byte[] dataBytes = MachineState.extractDataBytes(stateData);
return dataBytes;
} catch (ApiException e) {

View File

@@ -23,6 +23,7 @@ import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
@@ -480,4 +481,44 @@ public class BlocksResource {
}
}
@GET
@Path("/summaries")
@Operation(
summary = "Fetch only summary info about a range of blocks",
description = "Specify up to 2 out 3 of: start, end and count. If neither start nor end are specified, then end is assumed to be latest block. Where necessary, count is assumed to be 50.",
responses = {
@ApiResponse(
description = "blocks",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockInfo.class
)
)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public List<BlockInfo> getBlockRange(
@QueryParam("start") Integer startHeight,
@QueryParam("end") Integer endHeight,
@Parameter(ref = "count") @QueryParam("count") Integer count) {
// Check up to 2 out of 3 params
if (startHeight != null && endHeight != null && count != null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Check values
if ((startHeight != null && startHeight < 1) || (endHeight != null && endHeight < 1) || (count != null && count < 1))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockInfos(startHeight, endHeight, count);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,11 +6,12 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier;
import org.qortal.crypto.Crypto;
@@ -22,7 +23,7 @@ import org.qortal.repository.RepositoryManager;
@WebSocket
@SuppressWarnings("serial")
public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSocket {
public class ActiveChatsWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
@@ -31,7 +32,7 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
Map<String, String> pathParams = this.getPathParams(session, "/{address}");
Map<String, String> pathParams = getPathParams(session, "/{address}");
String address = pathParams.get("address");
if (address == null || !Crypto.isValidAddress(address)) {
@@ -52,6 +53,10 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
@@ -70,7 +75,7 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
StringWriter stringWriter = new StringWriter();
this.marshall(stringWriter, activeChats);
marshall(stringWriter, activeChats);
// Only output if something has changed
String output = stringWriter.toString();
@@ -78,8 +83,8 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
return;
previousOutput.set(output);
session.getRemote().sendString(output);
} catch (DataException | IOException e) {
session.getRemote().sendStringByFuture(output);
} catch (DataException | IOException | WebSocketException e) {
// No output this time?
}
}

View File

@@ -5,11 +5,12 @@ import java.io.StringWriter;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.NodeStatus;
import org.qortal.controller.StatusNotifier;
@@ -19,7 +20,7 @@ import org.qortal.repository.RepositoryManager;
@WebSocket
@SuppressWarnings("serial")
public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSocket {
public class AdminStatusWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
@@ -41,6 +42,10 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
StatusNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
@@ -51,7 +56,7 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
StringWriter stringWriter = new StringWriter();
this.marshall(stringWriter, nodeStatus);
marshall(stringWriter, nodeStatus);
// Only output if something has changed
String output = stringWriter.toString();
@@ -59,8 +64,8 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
return;
previousOutput.set(output);
session.getRemote().sendString(output);
} catch (DataException | IOException e) {
session.getRemote().sendStringByFuture(output);
} catch (DataException | IOException | WebSocketException e) {
// No output this time?
}
}

View File

@@ -3,7 +3,10 @@ package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
@@ -13,24 +16,28 @@ import javax.xml.bind.Marshaller;
import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrorRoot;
interface ApiWebSocket {
@SuppressWarnings("serial")
abstract class ApiWebSocket extends WebSocketServlet {
default String getPathInfo(Session session) {
private static final Map<Class<? extends ApiWebSocket>, List<Session>> SESSIONS_BY_CLASS = new HashMap<>();
protected static String getPathInfo(Session session) {
ServletUpgradeRequest upgradeRequest = (ServletUpgradeRequest) session.getUpgradeRequest();
return upgradeRequest.getHttpServletRequest().getPathInfo();
}
default Map<String, String> getPathParams(Session session, String pathSpec) {
protected static Map<String, String> getPathParams(Session session, String pathSpec) {
UriTemplatePathSpec uriTemplatePathSpec = new UriTemplatePathSpec(pathSpec);
return uriTemplatePathSpec.getPathParams(this.getPathInfo(session));
return uriTemplatePathSpec.getPathParams(getPathInfo(session));
}
default void sendError(Session session, ApiError apiError) {
protected static void sendError(Session session, ApiError apiError) {
ApiErrorRoot apiErrorRoot = new ApiErrorRoot();
apiErrorRoot.setApiError(apiError);
@@ -43,7 +50,7 @@ interface ApiWebSocket {
}
}
default void marshall(Writer writer, Object object) throws IOException {
protected static void marshall(Writer writer, Object object) throws IOException {
Marshaller marshaller = createMarshaller(object.getClass());
try {
@@ -53,7 +60,7 @@ interface ApiWebSocket {
}
}
default void marshall(Writer writer, Collection<?> collection) throws IOException {
protected static void marshall(Writer writer, Collection<?> collection) throws IOException {
// If collection is empty then we're returning "[]" anyway
if (collection.isEmpty()) {
writer.append("[]");
@@ -92,4 +99,22 @@ interface ApiWebSocket {
}
}
public void onWebSocketConnect(Session session) {
synchronized (SESSIONS_BY_CLASS) {
SESSIONS_BY_CLASS.computeIfAbsent(this.getClass(), clazz -> new ArrayList<>()).add(session);
}
}
public void onWebSocketClose(Session session, int statusCode, String reason) {
synchronized (SESSIONS_BY_CLASS) {
SESSIONS_BY_CLASS.get(this.getClass()).remove(session);
}
}
protected List<Session> getSessions() {
synchronized (SESSIONS_BY_CLASS) {
return new ArrayList<>(SESSIONS_BY_CLASS.get(this.getClass()));
}
}
}

View File

@@ -2,17 +2,19 @@ package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.ApiError;
import org.qortal.api.model.BlockInfo;
import org.qortal.controller.BlockNotifier;
import org.qortal.data.block.BlockData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -20,7 +22,7 @@ import org.qortal.utils.Base58;
@WebSocket
@SuppressWarnings("serial")
public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
public class BlocksWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
@@ -29,7 +31,7 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
BlockNotifier.Listener listener = blockData -> onNotify(session, blockData);
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo);
BlockNotifier.getInstance().register(session, listener);
}
@@ -38,6 +40,10 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
BlockNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
// We're expecting either a base58 block signature or an integer block height
@@ -53,13 +59,19 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
}
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
if (blockData == null) {
int height = repository.getBlockRepository().getHeightFromSignature(signature);
if (height == 0) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
onNotify(session, blockData);
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
if (blockInfos == null || blockInfos.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
onNotify(session, blockInfos.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
@@ -82,26 +94,26 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
}
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
if (blockData == null) {
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
if (blockInfos == null || blockInfos.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
onNotify(session, blockData);
onNotify(session, blockInfos.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
}
private void onNotify(Session session, BlockData blockData) {
private void onNotify(Session session, BlockInfo blockInfo) {
StringWriter stringWriter = new StringWriter();
try {
this.marshall(stringWriter, blockData);
marshall(stringWriter, blockInfo);
session.getRemote().sendString(stringWriter.toString());
} catch (IOException e) {
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time
}
}

View File

@@ -8,11 +8,12 @@ import java.util.List;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier;
import org.qortal.data.chat.ChatMessage;
@@ -23,7 +24,7 @@ import org.qortal.repository.RepositoryManager;
@WebSocket
@SuppressWarnings("serial")
public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSocket {
public class ChatMessagesWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
@@ -89,6 +90,10 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
@@ -123,10 +128,10 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
StringWriter stringWriter = new StringWriter();
try {
this.marshall(stringWriter, chatMessages);
marshall(stringWriter, chatMessages);
session.getRemote().sendString(stringWriter.toString());
} catch (IOException e) {
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time?
}
}

View File

@@ -0,0 +1,119 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.TradeBot;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
@WebSocket
@SuppressWarnings("serial")
public class TradeBotWebSocket extends ApiWebSocket implements Listener {
/** Cache of trade-bot entry states, keyed by trade-bot entry's "trade private key" (base58) */
private static final Map<String, TradeBotData.State> PREVIOUS_STATES = new HashMap<>();
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(TradeBotWebSocket.class);
try (final Repository repository = RepositoryManager.getRepository()) {
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
if (tradeBotEntries == null)
// How do we properly fail here?
return;
PREVIOUS_STATES.putAll(tradeBotEntries.stream().collect(Collectors.toMap(entry -> Base58.encode(entry.getTradePrivateKey()), TradeBotData::getState)));
} catch (DataException e) {
// No output this time
}
EventBus.INSTANCE.addListener(this::listen);
}
@Override
public void listen(Event event) {
if (!(event instanceof TradeBot.StateChangeEvent))
return;
TradeBotData tradeBotData = ((TradeBot.StateChangeEvent) event).getTradeBotData();
String tradePrivateKey58 = Base58.encode(tradeBotData.getTradePrivateKey());
synchronized (PREVIOUS_STATES) {
if (PREVIOUS_STATES.get(tradePrivateKey58) == tradeBotData.getState())
// Not changed
return;
PREVIOUS_STATES.put(tradePrivateKey58, tradeBotData.getState());
}
List<TradeBotData> tradeBotEntries = Collections.singletonList(tradeBotData);
for (Session session : getSessions())
sendEntries(session, tradeBotEntries);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
// Send all known trade-bot entries
try (final Repository repository = RepositoryManager.getRepository()) {
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
if (tradeBotEntries == null) {
session.close(4001, "repository issue fetching trade-bot entries");
return;
}
if (!sendEntries(session, tradeBotEntries)) {
session.close(4002, "websocket issue");
return;
}
} catch (DataException e) {
// No output this time
}
super.onWebSocketConnect(session);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private boolean sendEntries(Session session, List<TradeBotData> tradeBotEntries) {
try {
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, tradeBotEntries);
String output = stringWriter.toString();
session.getRemote().sendStringByFuture(output);
} catch (IOException e) {
// No output this time?
return false;
}
return true;
}
}

View File

@@ -0,0 +1,212 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.CrossChainOfferSummary;
import org.qortal.controller.BlockNotifier;
import org.qortal.crosschain.BTCACCT;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.NTP;
@WebSocket
@SuppressWarnings("serial")
public class TradeOffersWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(TradeOffersWebSocket.class);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
List<CrossChainOfferSummary> crossChainOfferSummaries;
try (final Repository repository = RepositoryManager.getRepository()) {
List<ATStateData> initialAtStates;
// We want ALL OFFERING trades
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET;
Long expectedValue = (long) BTCACCT.Mode.OFFERING.value;
Integer minimumFinalHeight = null;
initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (initialAtStates == null) {
session.close(4001, "repository issue fetching OFFERING trades");
return;
}
// Save initial AT modes
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING)));
// Convert to offer summaries
crossChainOfferSummaries = produceSummaries(repository, initialAtStates, null);
if (includeHistoric) {
// We also want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
long timestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
if (minimumFinalHeight != 0) {
isFinished = Boolean.TRUE;
dataByteOffset = null;
expectedValue = null;
++minimumFinalHeight; // because height is just *before* timestamp
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (historicAtStates == null) {
session.close(4002, "repository issue fetching historic trades");
return;
}
for (ATStateData historicAtState : historicAtStates) {
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, historicAtState, null);
switch (historicOfferSummary.getMode()) {
case REDEEMED:
case REFUNDED:
case CANCELLED:
break;
default:
continue;
}
// Add summary to initial burst
crossChainOfferSummaries.add(historicOfferSummary);
// Save initial AT mode
previousAtModes.put(historicAtState.getATAddress(), historicOfferSummary.getMode());
}
}
}
} catch (DataException e) {
session.close(4003, "generic repository issue");
return;
}
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
session.close(4004, "websocket issue");
return;
}
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo, previousAtModes);
BlockNotifier.getInstance().register(session, listener);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
BlockNotifier.getInstance().deregister(session);
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session, BlockInfo blockInfo, final Map<String, BTCACCT.Mode> previousAtModes) {
List<CrossChainOfferSummary> crossChainOfferSummaries = null;
try (final Repository repository = RepositoryManager.getRepository()) {
// Find any new trade ATs since this block
final Boolean isFinished = null;
final Integer dataByteOffset = null;
final Long expectedValue = null;
final Integer minimumFinalHeight = blockInfo.getHeight();
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (atStates == null)
return;
crossChainOfferSummaries = produceSummaries(repository, atStates, blockInfo.getTimestamp());
} catch (DataException e) {
// No output this time
}
synchronized (previousAtModes) { //NOSONAR squid:S2445 suppressed because previousAtModes is final and curried in lambda
// Remove any entries unchanged from last time
crossChainOfferSummaries.removeIf(offerSummary -> previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode());
// Don't send anything if no results
if (crossChainOfferSummaries.isEmpty())
return;
final boolean wasSent = sendOfferSummaries(session, crossChainOfferSummaries);
if (!wasSent)
return;
previousAtModes.putAll(crossChainOfferSummaries.stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, CrossChainOfferSummary::getMode)));
}
}
private boolean sendOfferSummaries(Session session, List<CrossChainOfferSummary> crossChainOfferSummaries) {
try {
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, crossChainOfferSummaries);
String output = stringWriter.toString();
session.getRemote().sendStringByFuture(output);
} catch (IOException e) {
// No output this time?
return false;
}
return true;
}
private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException {
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState);
long atStateTimestamp;
if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING)
// We want when trade was created, not when it was last updated
atStateTimestamp = atState.getCreation();
else
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
return new CrossChainOfferSummary(crossChainTradeData, atStateTimestamp);
}
private static List<CrossChainOfferSummary> produceSummaries(Repository repository, List<ATStateData> atStates, Long timestamp) throws DataException {
List<CrossChainOfferSummary> offerSummaries = new ArrayList<>();
for (ATStateData atState : atStates)
offerSummaries.add(produceSummary(repository, atState, timestamp));
return offerSummaries;
}
}

View File

@@ -117,12 +117,8 @@ public class AT {
}
public void update(int blockHeight, long blockTimestamp) throws DataException {
// [Re]create AT machine state using AT state data or from scratch as applicable
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, this.atStateData.getStateData(), codeBytes);
// Extract minimal/flags-only AT machine state using AT state data
MachineState state = MachineState.flagsOnlyfromBytes(this.atStateData.getStateData());
// Save latest AT state data
this.repository.getATRepository().save(this.atStateData);
@@ -151,12 +147,8 @@ public class AT {
if (previousStateData == null)
throw new DataException("Can't find previous AT state data for " + atAddress);
// [Re]create AT machine state using AT state data or from scratch as applicable
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, previousStateData.getStateData(), codeBytes);
// Extract minimal/flags-only AT machine state using AT state data
MachineState state = MachineState.flagsOnlyfromBytes(previousStateData.getStateData());
// Update AT info in repository
this.atData.setIsSleeping(state.isSleeping());

View File

@@ -17,7 +17,6 @@ import org.qortal.account.Account;
import org.qortal.account.NullAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockChain.CiyamAtSettings;
import org.qortal.crypto.Crypto;
@@ -30,13 +29,13 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.BlockRepository;
import org.qortal.repository.ATRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
import com.google.common.primitives.Bytes;
@@ -133,9 +132,9 @@ public class QortalATAPI extends API {
byte[] signature = blockSummaries.get(0).getSignature();
// Save some of minter's signature and transactions signature, so middle 24 bytes of the full 128 byte signature.
this.setA2(state, fromBytes(signature, 52));
this.setA3(state, fromBytes(signature, 60));
this.setA4(state, fromBytes(signature, 68));
this.setA2(state, BitTwiddling.longFromBEBytes(signature, 52));
this.setA3(state, BitTwiddling.longFromBEBytes(signature, 60));
this.setA4(state, BitTwiddling.longFromBEBytes(signature, 68));
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch previous block?", e);
}
@@ -149,59 +148,27 @@ public class QortalATAPI extends API {
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
BlockRepository blockRepository = this.getRepository().getBlockRepository();
ATRepository.NextTransactionInfo nextTransactionInfo;
try {
int currentHeight = blockRepository.getBlockchainHeight();
List<Transaction> blockTransactions = null;
while (height <= currentHeight) {
if (blockTransactions == null) {
BlockData blockData = blockRepository.fromHeight(height);
if (blockData == null)
throw new DataException("Unable to fetch block " + height + " from repository?");
Block block = new Block(this.getRepository(), blockData);
blockTransactions = block.getTransactions();
}
// No more transactions in this block? Try next block
if (sequence >= blockTransactions.size()) {
++height;
sequence = 0;
blockTransactions = null;
continue;
}
Transaction transaction = blockTransactions.get(sequence);
// Transaction needs to be sent to specified recipient
List<String> recipientAddresses = transaction.getRecipientAddresses();
if (recipientAddresses.contains(atAddress)) {
// Found a transaction
this.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
// Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction
byte[] signature = transaction.getTransactionData().getSignature();
this.setA2(state, fromBytes(signature, 8));
this.setA3(state, fromBytes(signature, 16));
this.setA4(state, fromBytes(signature, 24));
return;
}
// Transaction wasn't for us - keep going
++sequence;
}
// No more transactions - zero A and exit
this.zeroA(state);
nextTransactionInfo = this.getRepository().getATRepository().findNextTransaction(atAddress, height, sequence);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch next transaction?", e);
}
if (nextTransactionInfo == null) {
// No more transactions for AT at this time - zero A and exit
this.zeroA(state);
return;
}
// Found a transaction
this.setA1(state, new Timestamp(nextTransactionInfo.height, timestamp.blockchainId, nextTransactionInfo.sequence).longValue());
// Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction
this.setA2(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 8));
this.setA3(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 16));
this.setA4(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 24));
}
@Override
@@ -282,7 +249,7 @@ public class QortalATAPI extends API {
byte[] hash = Crypto.digest(input);
return fromBytes(hash, 0);
return BitTwiddling.longFromBEBytes(hash, 0);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch latest block from repository?", e);
}
@@ -296,30 +263,14 @@ public class QortalATAPI extends API {
TransactionData transactionData = this.getTransactionFromA(state);
byte[] messageData = null;
switch (transactionData.getType()) {
case MESSAGE:
messageData = ((MessageTransactionData) transactionData).getData();
break;
case AT:
messageData = ((ATTransactionData) transactionData).getMessage();
break;
default:
return;
}
// Check data length is appropriate, i.e. not larger than B
if (messageData.length > 4 * 8)
return;
byte[] messageData = this.getMessageFromTransaction(transactionData);
// Pad messageData to fit B
byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
if (messageData.length < 4 * 8)
messageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
this.setB(state, paddedMessageData);
this.setB(state, messageData);
}
@Override
@@ -457,12 +408,6 @@ public class QortalATAPI extends API {
// Utility methods
/** Convert part of little-endian byte[] to long */
/* package */ static long fromBytes(byte[] bytes, int start) {
return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
}
/** Returns partial transaction signature, used to verify we're operating on the same transaction and not naively using block height & sequence. */
public static byte[] partialSignature(byte[] fullSignature) {
return Arrays.copyOfRange(fullSignature, 8, 32);
@@ -473,7 +418,7 @@ public class QortalATAPI extends API {
// Compare end of transaction's signature against A2 thru A4
byte[] sig = transactionData.getSignature();
if (this.getA2(state) != fromBytes(sig, 8) || this.getA3(state) != fromBytes(sig, 16) || this.getA4(state) != fromBytes(sig, 24))
if (this.getA2(state) != BitTwiddling.longFromBEBytes(sig, 8) || this.getA3(state) != BitTwiddling.longFromBEBytes(sig, 16) || this.getA4(state) != BitTwiddling.longFromBEBytes(sig, 24))
throw new IllegalStateException("Transaction signature in A no longer matches signature from repository");
}
@@ -497,6 +442,20 @@ public class QortalATAPI extends API {
}
}
/** Returns message data from transaction. */
/*package*/ byte[] getMessageFromTransaction(TransactionData transactionData) {
switch (transactionData.getType()) {
case MESSAGE:
return ((MessageTransactionData) transactionData).getData();
case AT:
return ((ATTransactionData) transactionData).getMessage();
default:
return null;
}
}
/** Returns AT's account */
/* package */ Account getATAccount() {
return new Account(this.repository, this.atData.getATAddress());
@@ -563,4 +522,8 @@ public class QortalATAPI extends API {
super.setB(state, bBytes);
}
protected void zeroB(MachineState state) {
super.zeroB(state);
}
}

View File

@@ -691,6 +691,11 @@ public final class QortalAtLogger extends ExtendedLoggerWrapper implements org.c
logger.logIfEnabled(FQCN, ERROR, null, msgSupplier, (Throwable) null);
}
/** Java 8 version */
public void error(final java.util.function.Supplier<String> msgSupplier) {
logger.logIfEnabled(FQCN, ERROR, null, () -> msgSupplier.get(), (Throwable) null);
}
/**
* Logs a message (only to be constructed if the logging level is the {@code ERROR}
* level) including the stack trace of the {@link Throwable} <code>t</code> passed as parameter.
@@ -1375,6 +1380,11 @@ public final class QortalAtLogger extends ExtendedLoggerWrapper implements org.c
logger.logIfEnabled(FQCN, DEBUG, null, msgSupplier, (Throwable) null);
}
/** Java 8 version */
public void debug(final java.util.function.Supplier<String> msgSupplier) {
logger.logIfEnabled(FQCN, DEBUG, null, () -> msgSupplier.get(), (Throwable) null);
}
/**
* Logs a message (only to be constructed if the logging level is the {@code DEBUG}
* level) including the stack trace of the {@link Throwable} <code>t</code> passed as parameter.
@@ -2059,6 +2069,11 @@ public final class QortalAtLogger extends ExtendedLoggerWrapper implements org.c
logger.logIfEnabled(FQCN, ECHO, null, msgSupplier, (Throwable) null);
}
/** Java 8 version */
public void echo(final java.util.function.Supplier<String> msgSupplier) {
logger.logIfEnabled(FQCN, ECHO, null, () -> msgSupplier.get(), (Throwable) null);
}
/**
* Logs a message (only to be constructed if the logging level is the {@code ECHO}
* level) including the stack trace of the {@link Throwable} <code>t</code> passed as parameter.

View File

@@ -12,6 +12,7 @@ import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
import org.qortal.crosschain.BTC;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.TransactionData;
import org.qortal.settings.Settings;
/**
@@ -22,8 +23,70 @@ import org.qortal.settings.Settings;
*/
public enum QortalFunctionCode {
/**
* <tt>0x0510</tt><br>
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.
* Returns length of message data from transaction in A.<br>
* <tt>0x0501</tt><br>
* If transaction has no 'message', returns -1.
*/
GET_MESSAGE_LENGTH_FROM_TX_IN_A(0x0501, 0, true) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
QortalATAPI api = (QortalATAPI) state.getAPI();
TransactionData transactionData = api.getTransactionFromA(state);
byte[] messageData = api.getMessageFromTransaction(transactionData);
if (messageData == null)
functionData.returnValue = -1L;
else
functionData.returnValue = (long) messageData.length;
}
},
/**
* Put offset 'message' from transaction in A into B<br>
* <tt>0x0502 start-offset</tt><br>
* Copies up to 32 bytes of message data, starting at <tt>start-offset</tt> into B.<br>
* If transaction has no 'message', or <tt>start-offset</tt> out of bounds, then zero B<br>
* Example 'message' could be 256-bit shared secret
*/
PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B(0x0502, 1, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
QortalATAPI api = (QortalATAPI) state.getAPI();
// In case something goes wrong, or we don't have enough message data.
api.zeroB(state);
if (functionData.value1 < 0 || functionData.value1 > Integer.MAX_VALUE)
return;
int startOffset = functionData.value1.intValue();
TransactionData transactionData = api.getTransactionFromA(state);
byte[] messageData = api.getMessageFromTransaction(transactionData);
if (messageData == null || startOffset > messageData.length)
return;
/*
* Copy up to 32 bytes of message data into B,
* retain order but pad with zeros in lower bytes.
*
* So a 4-byte message "a b c d" would copy thusly:
* a b c d 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
*/
int byteCount = Math.min(32, messageData.length - startOffset);
byte[] bBytes = new byte[32];
System.arraycopy(messageData, startOffset, bBytes, 0, byteCount);
api.setB(state, bBytes);
}
},
/**
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.<br>
* <tt>0x0510</tt>
*/
CONVERT_B_TO_PKH(0x0510, 0, false) {
@Override
@@ -38,8 +101,8 @@ public enum QortalFunctionCode {
}
},
/**
* <tt>0x0511</tt><br>
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.<br>
* <tt>0x0511</tt><br>
* P2SH stored in lower 25 bytes of B.
*/
CONVERT_B_TO_P2SH(0x0511, 0, false) {
@@ -51,8 +114,8 @@ public enum QortalFunctionCode {
}
},
/**
* <tt>0x0512</tt><br>
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.<br>
* <tt>0x0512</tt><br>
* Qortal address stored in lower 25 bytes of B.
*/
CONVERT_B_TO_QORTAL(0x0512, 0, false) {

View File

@@ -215,6 +215,9 @@ public class Block {
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
private List<ExpandedAccount> cachedExpandedAccounts = null;
/** Opportunistic cache of this block's valid online accounts. Only created by call to isValid(). */
private List<OnlineAccountData> cachedValidOnlineAccounts = null;
// Other useful constants
private static final BigInteger MAX_DISTANCE;
@@ -940,24 +943,46 @@ public class Block {
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
// Check signatures
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp);
List<OnlineAccountData> onlineAccounts = Controller.getInstance().getOnlineAccounts();
// If this block is much older than current online timestamp, then there's no point checking current online accounts
List<OnlineAccountData> currentOnlineAccounts = onlineTimestamp < NTP.getTime() - Controller.ONLINE_TIMESTAMP_MODULUS
? null
: Controller.getInstance().getOnlineAccounts();
List<OnlineAccountData> latestBlocksOnlineAccounts = Controller.getInstance().getLatestBlocksOnlineAccounts();
// Extract online accounts' timestamp signatures from block data
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
// We'll build up a list of online accounts to hand over to Controller if block is added to chain
// and this will become latestBlocksOnlineAccounts (above) to reduce CPU load when we process next block...
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
byte[] signature = onlineAccountsSignatures.get(i);
byte[] publicKey = expandedAccounts.get(i).getRewardSharePublicKey();
// If signature is still current then no need to perform Ed25519 verify
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
if (onlineAccounts.remove(onlineAccountData)) // remove() is like contains() but also reduces the number to check next time
ourOnlineAccounts.add(onlineAccountData);
// If signature is still current then no need to perform Ed25519 verify
if (currentOnlineAccounts != null && currentOnlineAccounts.remove(onlineAccountData))
// remove() returned true, so online account still current
// and one less entry in currentOnlineAccounts to check next time
continue;
// If signature was okay in latest block then no need to perform Ed25519 verify
if (latestBlocksOnlineAccounts != null && latestBlocksOnlineAccounts.contains(onlineAccountData))
continue;
if (!Crypto.verify(publicKey, signature, onlineTimestampBytes))
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
}
// All online accounts valid, so save our list of online accounts for potential later use
this.cachedValidOnlineAccounts = ourOnlineAccounts;
return ValidationResult.OK;
}
@@ -1271,6 +1296,9 @@ public class Block {
linkTransactionsToBlock();
postBlockTidy();
// Give Controller our cached, valid online accounts data (if any) to help reduce CPU load for next block
Controller.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts);
}
protected void increaseAccountLevels() throws DataException {
@@ -1474,6 +1502,9 @@ public class Block {
this.blockData.setHeight(null);
postBlockTidy();
// Remove any cached, valid online accounts data from Controller
Controller.getInstance().popLatestBlocksOnlineAccounts();
}
protected void orphanTransactionsFromBlock() throws DataException {

View File

@@ -482,7 +482,7 @@ public class BlockChain {
}
/**
* Some sort start-up/initialization/checking method.
* Some sort of start-up/initialization/checking method.
*
* @throws SQLException
*/
@@ -492,7 +492,9 @@ public class BlockChain {
rebuildBlockchain();
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature();
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - 1440, 1);
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
if (detachedBlockData != null) {
LOGGER.error(String.format("Block %d's reference does not match any block's signature", detachedBlockData.getHeight()));

View File

@@ -214,8 +214,9 @@ public class AutoUpdate extends Thread {
return false; // failed - try another repo
}
// Give repository a chance to backup in case things go badly wrong
RepositoryManager.backup(true);
// Give repository a chance to backup in case things go badly wrong (if enabled)
if (Settings.getInstance().getRepositoryBackupInterval() > 0)
RepositoryManager.backup(true);
// Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced)
String javaHome = System.getProperty("java.home");

View File

@@ -1,9 +1,12 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.qortal.api.model.BlockInfo;
import org.qortal.data.block.BlockData;
public class BlockNotifier {
@@ -12,7 +15,7 @@ public class BlockNotifier {
@FunctionalInterface
public interface Listener {
void notify(BlockData blockData);
void notify(BlockInfo blockInfo);
}
private Map<Session, Listener> listenersBySession = new HashMap<>();
@@ -27,17 +30,32 @@ public class BlockNotifier {
return instance;
}
public synchronized void register(Session session, Listener listener) {
this.listenersBySession.put(session, listener);
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public synchronized void deregister(Session session) {
this.listenersBySession.remove(session);
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public synchronized void onNewBlock(BlockData blockData) {
for (Listener listener : this.listenersBySession.values())
listener.notify(blockData);
public void onNewBlock(BlockData blockData) {
// Convert BlockData to BlockInfo
BlockInfo blockInfo = new BlockInfo(blockData);
for (Listener listener : getAllListeners())
listener.notify(blockInfo);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -1,5 +1,7 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -27,22 +29,34 @@ public class ChatNotifier {
return instance;
}
public synchronized void register(Session session, Listener listener) {
this.listenersBySession.put(session, listener);
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public synchronized void deregister(Session session) {
this.listenersBySession.remove(session);
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public synchronized void onNewChatTransaction(ChatTransactionData chatTransactionData) {
for (Listener listener : this.listenersBySession.values())
public void onNewChatTransaction(ChatTransactionData chatTransactionData) {
for (Listener listener : getAllListeners())
listener.notify(chatTransactionData);
}
public synchronized void onGroupMembershipChange() {
for (Listener listener : this.listenersBySession.values())
public void onGroupMembershipChange() {
for (Listener listener : getAllListeners())
listener.notify(null);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -9,9 +9,11 @@ import java.security.Security;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -112,8 +114,10 @@ public class Controller extends Thread {
// To do with online accounts list
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 1 * 60 * 1000L; // ms
private static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000L;
public static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000L;
private static final long LAST_SEEN_EXPIRY_PERIOD = (ONLINE_TIMESTAMP_MODULUS * 2) + (1 * 60 * 1000L);
/** How many (latest) blocks' worth of online accounts we cache */
private static final int MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS = 2;
private static volatile boolean isStopping = false;
private static BlockMinter blockMinter = null;
@@ -168,8 +172,10 @@ public class Controller extends Thread {
/** Lock for only allowing one blockchain-modifying codepath at a time. e.g. synchronization or newly minted block. */
private final ReentrantLock blockchainLock = new ReentrantLock();
/** Cache of 'online accounts' */
/** Cache of current 'online accounts' */
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
/** Cache of latest blocks' online accounts */
Deque<List<OnlineAccountData>> latestBlocksOnlineAccounts = new ArrayDeque<>(MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS);
// Constructors
@@ -537,8 +543,11 @@ public class Controller extends Thread {
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
isSynchronizing = true;
updateSysTray();
// Only update SysTray if we're potentially changing height
if (syncPercent < 100) {
isSynchronizing = true;
updateSysTray();
}
BlockData priorChainTip = this.chainTip;
@@ -584,7 +593,6 @@ public class Controller extends Thread {
break;
case OK:
requestSysTrayUpdate = true;
// fall-through...
case NOTHING_TO_DO: {
// Update our list of inferior chain tips
@@ -611,14 +619,13 @@ public class Controller extends Thread {
// Reset our cache of inferior chains
inferiorChainSignatures.clear();
// Update chain-tip, notify peers, websockets, etc.
// Update chain-tip, systray, notify peers, websockets, etc.
this.onNewBlock(newChainTip);
}
return syncResult;
} finally {
isSynchronizing = false;
requestSysTrayUpdate = true;
}
}
@@ -765,8 +772,10 @@ public class Controller extends Thread {
BlockData latestBlockData = getChainTip();
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
// Send (if outbound) / Request unconfirmed transaction signatures
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
// Request unconfirmed transaction signatures, but only if we're up-to-date.
// If we're NOT up-to-date then priority is synchronizing first
if (isUpToDate())
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
}
public void onMintingPossibleChange(boolean isMintingPossible) {
@@ -789,6 +798,9 @@ public class Controller extends Thread {
this.notifyGroupMembershipChange = false;
ChatNotifier.getInstance().onGroupMembershipChange();
}
// Trade-bot might want to perform some actions too
TradeBot.getInstance().onChainTipChange();
});
}
@@ -888,8 +900,16 @@ public class Controller extends Thread {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
if (blockData == null) {
LOGGER.debug(() -> String.format("Ignoring GET_BLOCK request from peer %s for unknown block %s", peer, Base58.encode(signature)));
// Send no response at all???
// We don't have this block
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
// We'll send empty block summaries message as it's very short
Message blockUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
blockUnknownMessage.setId(message.getId());
if (!peer.sendMessage(blockUnknownMessage))
peer.disconnect("failed to send block-unknown response");
return;
}
@@ -1040,7 +1060,12 @@ public class Controller extends Thread {
private void onNetworkGetUnconfirmedTransactionsMessage(Peer peer, Message message) {
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
List<byte[]> signatures = Collections.emptyList();
// If we're NOT up-to-date then don't send out unconfirmed transactions
// as it's possible they are already included in a later block that we don't have.
if (isUpToDate())
signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
Message transactionSignaturesMessage = new TransactionSignaturesMessage(signatures);
if (!peer.sendMessage(transactionSignaturesMessage))
@@ -1446,6 +1471,32 @@ public class Controller extends Thread {
}
}
/** Returns cached, unmodifiable list of latest block's online accounts. */
public List<OnlineAccountData> getLatestBlocksOnlineAccounts() {
synchronized (this.latestBlocksOnlineAccounts) {
return this.latestBlocksOnlineAccounts.peekFirst();
}
}
/** Caches list of latest block's online accounts. Typically called by Block.process() */
public void pushLatestBlocksOnlineAccounts(List<OnlineAccountData> latestBlocksOnlineAccounts) {
synchronized (this.latestBlocksOnlineAccounts) {
if (this.latestBlocksOnlineAccounts.size() == MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS)
this.latestBlocksOnlineAccounts.pollLast();
this.latestBlocksOnlineAccounts.addFirst(latestBlocksOnlineAccounts == null
? Collections.emptyList()
: Collections.unmodifiableList(latestBlocksOnlineAccounts));
}
}
/** Reverts list of latest block's online accounts. Typically called by Block.orphan() */
public void popLatestBlocksOnlineAccounts() {
synchronized (this.latestBlocksOnlineAccounts) {
this.latestBlocksOnlineAccounts.pollFirst();
}
}
public byte[] fetchArbitraryData(byte[] signature) throws InterruptedException {
// Build request
Message getArbitraryDataMessage = new GetArbitraryDataMessage(signature);

View File

@@ -1,5 +1,7 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -26,17 +28,29 @@ public class StatusNotifier {
return instance;
}
public synchronized void register(Session session, Listener listener) {
this.listenersBySession.put(session, listener);
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public synchronized void deregister(Session session) {
this.listenersBySession.remove(session);
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public synchronized void onStatusChange(long now) {
for (Listener listener : this.listenersBySession.values())
public void onStatusChange(long now) {
for (Listener listener : getAllListeners())
listener.notify(now);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -126,167 +126,22 @@ public class Synchronizer {
// Unless we're doing a forced sync, we might need to compare blocks after common block
if (!force && ourInitialHeight > commonBlockHeight) {
// If our latest block is very old, we're very behind and should ditch our fork.
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
if (minLatestBlockTimestamp == null)
return SynchronizationResult.REPOSITORY_ISSUE;
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
LOGGER.info(String.format("Ditching our chain after height %d as our latest block is very old", commonBlockHeight));
} else {
// Compare chain weights
LOGGER.debug(String.format("Comparing chains from block %d with peer %s", commonBlockHeight + 1, peer));
// Fetch remaining peer's block summaries (which we also use to fill signatures list)
int peerBlockCount = peerHeight - commonBlockHeight;
while (peerBlockSummaries.size() < peerBlockCount) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
int lastSummaryHeight = commonBlockHeight + peerBlockSummaries.size();
byte[] previousSignature;
if (peerBlockSummaries.isEmpty())
previousSignature = commonBlockSig;
else
previousSignature = peerBlockSummaries.get(peerBlockSummaries.size() - 1).getSignature();
List<BlockSummaryData> moreBlockSummaries = this.getBlockSummaries(peer, previousSignature, peerBlockCount - peerBlockSummaries.size());
if (moreBlockSummaries == null || moreBlockSummaries.isEmpty()) {
LOGGER.info(String.format("Peer %s failed to respond with block summaries after height %d, sig %.8s", peer,
lastSummaryHeight, Base58.encode(previousSignature)));
return SynchronizationResult.NO_REPLY;
}
// Check peer sent valid heights
for (int i = 0; i < moreBlockSummaries.size(); ++i) {
++lastSummaryHeight;
BlockSummaryData blockSummary = moreBlockSummaries.get(i);
if (blockSummary.getHeight() != lastSummaryHeight) {
LOGGER.info(String.format("Peer %s responded with invalid block summary for height %d, sig %.8s", peer,
lastSummaryHeight, Base58.encode(blockSummary.getSignature())));
return SynchronizationResult.NO_REPLY;
}
}
peerBlockSummaries.addAll(moreBlockSummaries);
}
// Fetch our corresponding block summaries
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourInitialHeight);
// Populate minter account levels for both lists of block summaries
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
// If our blockchain has greater weight then don't synchronize with peer
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
NumberFormat formatter = new DecimalFormat("0.###E0");
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
return SynchronizationResult.INFERIOR_CHAIN;
}
}
SynchronizationResult chainCompareResult = compareChains(repository, commonBlockData, ourLatestBlockData, peer, peerHeight, peerBlockSummaries);
if (chainCompareResult != SynchronizationResult.OK)
return chainCompareResult;
}
int ourHeight = ourInitialHeight;
if (ourHeight > commonBlockHeight) {
// Unwind to common block (unless common block is our latest block)
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58));
while (ourHeight > commonBlockHeight) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
BlockData blockData = repository.getBlockRepository().fromHeight(ourHeight);
Block block = new Block(repository, blockData);
block.orphan();
--ourHeight;
}
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - fetching blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
SynchronizationResult syncResult = null;
if (commonBlockHeight < ourInitialHeight) {
// Peer's chain is better, sync to that one
syncResult = syncToPeerChain(repository, commonBlockData, ourInitialHeight, peer, peerHeight, peerBlockSummaries);
} else {
LOGGER.debug(String.format("Fetching new blocks from peer %s", peer));
// Simply fetch and apply blocks as they arrive
syncResult = applyNewBlocks(repository, commonBlockData, ourInitialHeight, peer, peerHeight, peerBlockSummaries);
}
// Fetch, and apply, blocks from peer
byte[] latestPeerSignature = commonBlockSig;
int maxBatchHeight = commonBlockHeight + SYNC_BATCH_SIZE;
// Convert any block summaries from above into signatures to request from peer
List<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList());
while (ourHeight < peerHeight && ourHeight < maxBatchHeight) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
// Do we need more signatures?
if (peerBlockSignatures.isEmpty()) {
int numberRequested = maxBatchHeight - ourHeight;
LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s",
numberRequested, (numberRequested != 1 ? "s": ""), ourHeight, Base58.encode(latestPeerSignature)));
peerBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberRequested);
if (peerBlockSignatures == null || peerBlockSignatures.isEmpty()) {
LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer,
ourHeight, Base58.encode(latestPeerSignature)));
return SynchronizationResult.NO_REPLY;
}
LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : "")));
}
latestPeerSignature = peerBlockSignatures.get(0);
peerBlockSignatures.remove(0);
++ourHeight;
Block newBlock = this.fetchBlock(repository, peer, latestPeerSignature);
if (newBlock == null) {
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
ourHeight, Base58.encode(latestPeerSignature)));
return SynchronizationResult.NO_REPLY;
}
if (!newBlock.isSignatureValid()) {
LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer,
ourHeight, Base58.encode(latestPeerSignature)));
return SynchronizationResult.INVALID_DATA;
}
// Transactions are transmitted without approval status so determine that now
for (Transaction transaction : newBlock.getTransactions())
transaction.setInitialApprovalStatus();
ValidationResult blockResult = newBlock.isValid();
if (blockResult != ValidationResult.OK) {
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
ourHeight, Base58.encode(latestPeerSignature), blockResult.name()));
return SynchronizationResult.INVALID_DATA;
}
// Save transactions attached to this block
for (Transaction transaction : newBlock.getTransactions()) {
TransactionData transactionData = transaction.getTransactionData();
repository.getTransactionRepository().save(transactionData);
}
newBlock.process();
// If we've grown our blockchain then at least save progress so far
if (ourHeight > ourInitialHeight)
repository.saveChanges();
}
if (syncResult != SynchronizationResult.OK)
return syncResult;
// Commit
repository.saveChanges();
@@ -396,6 +251,268 @@ public class Synchronizer {
return SynchronizationResult.OK;
}
private SynchronizationResult compareChains(Repository repository, BlockData commonBlockData, BlockData ourLatestBlockData,
Peer peer, int peerHeight, List<BlockSummaryData> peerBlockSummaries) throws DataException, InterruptedException {
final int commonBlockHeight = commonBlockData.getHeight();
final byte[] commonBlockSig = commonBlockData.getSignature();
// If our latest block is very old, we're very behind and should ditch our fork.
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
if (minLatestBlockTimestamp == null)
return SynchronizationResult.REPOSITORY_ISSUE;
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
LOGGER.info(String.format("Ditching our chain after height %d as our latest block is very old", commonBlockHeight));
} else {
// Compare chain weights
LOGGER.debug(String.format("Comparing chains from block %d with peer %s", commonBlockHeight + 1, peer));
// Fetch remaining peer's block summaries (which we also use to fill signatures list)
int peerBlockCount = peerHeight - commonBlockHeight;
while (peerBlockSummaries.size() < peerBlockCount) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
int lastSummaryHeight = commonBlockHeight + peerBlockSummaries.size();
byte[] previousSignature;
if (peerBlockSummaries.isEmpty())
previousSignature = commonBlockSig;
else
previousSignature = peerBlockSummaries.get(peerBlockSummaries.size() - 1).getSignature();
List<BlockSummaryData> moreBlockSummaries = this.getBlockSummaries(peer, previousSignature, peerBlockCount - peerBlockSummaries.size());
if (moreBlockSummaries == null || moreBlockSummaries.isEmpty()) {
LOGGER.info(String.format("Peer %s failed to respond with block summaries after height %d, sig %.8s", peer,
lastSummaryHeight, Base58.encode(previousSignature)));
return SynchronizationResult.NO_REPLY;
}
// Check peer sent valid heights
for (int i = 0; i < moreBlockSummaries.size(); ++i) {
++lastSummaryHeight;
BlockSummaryData blockSummary = moreBlockSummaries.get(i);
if (blockSummary.getHeight() != lastSummaryHeight) {
LOGGER.info(String.format("Peer %s responded with invalid block summary for height %d, sig %.8s", peer,
lastSummaryHeight, Base58.encode(blockSummary.getSignature())));
return SynchronizationResult.NO_REPLY;
}
}
peerBlockSummaries.addAll(moreBlockSummaries);
}
// Fetch our corresponding block summaries
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
// Populate minter account levels for both lists of block summaries
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
// If our blockchain has greater weight then don't synchronize with peer
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
NumberFormat formatter = new DecimalFormat("0.###E0");
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
return SynchronizationResult.INFERIOR_CHAIN;
}
}
return SynchronizationResult.OK;
}
private SynchronizationResult syncToPeerChain(Repository repository, BlockData commonBlockData, int ourInitialHeight,
Peer peer, final int peerHeight, List<BlockSummaryData> peerBlockSummaries) throws DataException, InterruptedException {
final int commonBlockHeight = commonBlockData.getHeight();
final byte[] commonBlockSig = commonBlockData.getSignature();
String commonBlockSig58 = Base58.encode(commonBlockSig);
LOGGER.debug(() -> String.format("Fetching peer %s chain from height %d, sig %.8s", peer, commonBlockHeight, commonBlockSig58));
int ourHeight = ourInitialHeight;
// Overall plan: fetch peer's blocks first, then orphan, then apply
// Convert any leftover (post-common) block summaries into signatures to request from peer
List<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList());
// Fetch remaining block signatures, if needed
int numberSignaturesRequired = peerBlockSignatures.size() - (peerHeight - commonBlockHeight);
if (numberSignaturesRequired > 0) {
byte[] latestPeerSignature = peerBlockSignatures.isEmpty() ? commonBlockSig : peerBlockSignatures.get(peerBlockSignatures.size() - 1);
LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s",
numberSignaturesRequired, (numberSignaturesRequired != 1 ? "s": ""), ourHeight, Base58.encode(latestPeerSignature)));
List<byte[]> moreBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberSignaturesRequired);
if (moreBlockSignatures == null || moreBlockSignatures.isEmpty()) {
LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer,
ourHeight, Base58.encode(latestPeerSignature)));
return SynchronizationResult.NO_REPLY;
}
LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : "")));
peerBlockSignatures.addAll(moreBlockSignatures);
}
// Fetch blocks using signatures
LOGGER.debug(String.format("Fetching new blocks from peer %s", peer));
List<Block> peerBlocks = new ArrayList<>();
for (byte[] blockSignature : peerBlockSignatures) {
Block newBlock = this.fetchBlock(repository, peer, blockSignature);
if (newBlock == null) {
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
ourHeight, Base58.encode(blockSignature)));
return SynchronizationResult.NO_REPLY;
}
if (!newBlock.isSignatureValid()) {
LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer,
ourHeight, Base58.encode(blockSignature)));
return SynchronizationResult.INVALID_DATA;
}
// Transactions are transmitted without approval status so determine that now
for (Transaction transaction : newBlock.getTransactions())
transaction.setInitialApprovalStatus();
peerBlocks.add(newBlock);
}
// Unwind to common block (unless common block is our latest block)
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58));
while (ourHeight > commonBlockHeight) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
BlockData blockData = repository.getBlockRepository().fromHeight(ourHeight);
Block block = new Block(repository, blockData);
block.orphan();
--ourHeight;
}
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - applying new blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
for (Block newBlock : peerBlocks) {
ValidationResult blockResult = newBlock.isValid();
if (blockResult != ValidationResult.OK) {
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
ourHeight, Base58.encode(newBlock.getSignature()), blockResult.name()));
return SynchronizationResult.INVALID_DATA;
}
// Save transactions attached to this block
for (Transaction transaction : newBlock.getTransactions()) {
TransactionData transactionData = transaction.getTransactionData();
repository.getTransactionRepository().save(transactionData);
}
newBlock.process();
// If we've grown our blockchain then at least save progress so far
if (ourHeight > ourInitialHeight)
repository.saveChanges();
}
return SynchronizationResult.OK;
}
private SynchronizationResult applyNewBlocks(Repository repository, BlockData commonBlockData, int ourInitialHeight,
Peer peer, int peerHeight, List<BlockSummaryData> peerBlockSummaries) throws InterruptedException, DataException {
LOGGER.debug(String.format("Fetching new blocks from peer %s", peer));
final int commonBlockHeight = commonBlockData.getHeight();
final byte[] commonBlockSig = commonBlockData.getSignature();
int ourHeight = ourInitialHeight;
// Fetch, and apply, blocks from peer
byte[] latestPeerSignature = commonBlockSig;
int maxBatchHeight = commonBlockHeight + SYNC_BATCH_SIZE;
// Convert any block summaries from above into signatures to request from peer
List<byte[]> peerBlockSignatures = peerBlockSummaries.stream().map(BlockSummaryData::getSignature).collect(Collectors.toList());
while (ourHeight < peerHeight && ourHeight < maxBatchHeight) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
// Do we need more signatures?
if (peerBlockSignatures.isEmpty()) {
int numberRequested = maxBatchHeight - ourHeight;
LOGGER.trace(String.format("Requesting %d signature%s after height %d, sig %.8s",
numberRequested, (numberRequested != 1 ? "s": ""), ourHeight, Base58.encode(latestPeerSignature)));
peerBlockSignatures = this.getBlockSignatures(peer, latestPeerSignature, numberRequested);
if (peerBlockSignatures == null || peerBlockSignatures.isEmpty()) {
LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer,
ourHeight, Base58.encode(latestPeerSignature)));
return SynchronizationResult.NO_REPLY;
}
LOGGER.trace(String.format("Received %s signature%s", peerBlockSignatures.size(), (peerBlockSignatures.size() != 1 ? "s" : "")));
}
latestPeerSignature = peerBlockSignatures.get(0);
peerBlockSignatures.remove(0);
++ourHeight;
Block newBlock = this.fetchBlock(repository, peer, latestPeerSignature);
if (newBlock == null) {
LOGGER.info(String.format("Peer %s failed to respond with block for height %d, sig %.8s", peer,
ourHeight, Base58.encode(latestPeerSignature)));
return SynchronizationResult.NO_REPLY;
}
if (!newBlock.isSignatureValid()) {
LOGGER.info(String.format("Peer %s sent block with invalid signature for height %d, sig %.8s", peer,
ourHeight, Base58.encode(latestPeerSignature)));
return SynchronizationResult.INVALID_DATA;
}
// Transactions are transmitted without approval status so determine that now
for (Transaction transaction : newBlock.getTransactions())
transaction.setInitialApprovalStatus();
ValidationResult blockResult = newBlock.isValid();
if (blockResult != ValidationResult.OK) {
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
ourHeight, Base58.encode(latestPeerSignature), blockResult.name()));
return SynchronizationResult.INVALID_DATA;
}
// Save transactions attached to this block
for (Transaction transaction : newBlock.getTransactions()) {
TransactionData transactionData = transaction.getTransactionData();
repository.getTransactionRepository().save(transactionData);
}
newBlock.process();
// If we've grown our blockchain then at least save progress so far
if (ourHeight > ourInitialHeight)
repository.saveChanges();
}
return SynchronizationResult.OK;
}
private List<BlockSummaryData> getBlockSummaries(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
Message getBlockSummariesMessage = new GetBlockSummariesMessage(parentSignature, numberRequested);

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,45 @@
package org.qortal.crosschain;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.UTXO;
import org.bitcoinj.core.UTXOProvider;
import org.bitcoinj.core.UTXOProviderException;
import org.bitcoinj.crypto.DeterministicHierarchy;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.utils.MonetaryFormat;
import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.qortal.crosschain.ElectrumX.UnspentOutput;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.utils.BitTwiddling;
import org.qortal.utils.Pair;
import com.google.common.hash.HashCode;
public class BTC {
public static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
public static final int HASH160_LENGTH = 20;
@@ -30,6 +47,7 @@ public class BTC {
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
public enum BitcoinNet {
MAIN {
@@ -58,6 +76,9 @@ public class BTC {
private final NetworkParameters params;
private final ElectrumX electrumX;
// Let ECKey.equals() do the hard work
private final Set<ECKey> spentKeys = new HashSet<>();
// Constructors and instance
private BTC() {
@@ -88,6 +109,34 @@ public class BTC {
// Actual useful methods for use by other classes
public static String format(Coin amount) {
return BTC.FORMAT.format(amount).toString();
}
public static String format(long amount) {
return format(Coin.valueOf(amount));
}
public boolean isValidXprv(String xprv58) {
try {
DeterministicKey.deserializeB58(null, xprv58, this.params);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
/** Returns P2PKH Bitcoin address using passed public key hash. */
public String pkhToAddress(byte[] publicKeyHash) {
return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
}
public String deriveP2shAddress(byte[] redeemScriptBytes) {
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
return p2shAddress.toString();
}
/** Returns median timestamp from latest 11 blocks, in seconds. */
public Integer getMedianBlockTime() {
Integer height = this.electrumX.getCurrentHeight();
@@ -99,34 +148,31 @@ public class BTC {
if (blockHeaders == null || blockHeaders.size() < 11)
return null;
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.fromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
// Descending, but order shouldn't matter as we're picking median...
// Descending order
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
// Pick median
return blockTimestamps.get(5);
}
public Coin getBalance(String base58Address) {
Long balance = this.electrumX.getBalance(addressToScript(base58Address));
if (balance == null)
return null;
return Coin.valueOf(balance);
public Long getBalance(String base58Address) {
return this.electrumX.getBalance(addressToScript(base58Address));
}
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
List<Pair<byte[], Integer>> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
if (unspentOutputs == null)
return null;
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (Pair<byte[], Integer> unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.getA());
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
return null;
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.getB()));
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.index));
}
return unspentTransactionOutputs;
@@ -141,6 +187,7 @@ public class BTC {
return transaction.getOutputs();
}
/** Returns list of raw transactions spending passed address. */
public List<byte[]> getAddressTransactions(String base58Address) {
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
}
@@ -149,6 +196,181 @@ public class BTC {
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
}
/**
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt>.
*
* @param xprv58 BIP32 extended Bitcoin private key
* @param recipient P2PKH address
* @param amount unscaled amount
* @return transaction, or null if insufficient funds
*/
public Transaction buildSpend(String xprv58, String recipient, long amount) {
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ALL_SPENT));
Address destination = Address.fromString(this.params, recipient);
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
if (this.params == TestNet3Params.get())
// Much smaller fee for TestNet3
sendRequest.feePerKb = Coin.valueOf(2000L);
try {
wallet.completeTx(sendRequest);
return sendRequest.tx;
} catch (InsufficientMoneyException e) {
return null;
}
}
/**
* Returns unspent Bitcoin balance given 'm' BIP32 key.
*
* @param xprv58 BIP32 extended Bitcoin private key
* @return unspent BTC balance, or null if unable to determine balance
*/
public Long getWalletBalance(String xprv58) {
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
Coin balance = wallet.getBalance();
if (balance == null)
return null;
return balance.value;
}
// UTXOProvider support
static class WalletAwareUTXOProvider implements UTXOProvider {
private static final int LOOKAHEAD_INCREMENT = 3;
private final BTC btc;
private final Wallet wallet;
enum KeySearchMode {
REQUEST_MORE_IF_ALL_SPENT, REQUEST_MORE_IF_ANY_SPENT;
}
private final KeySearchMode keySearchMode;
private final DeterministicKeyChain keyChain;
public WalletAwareUTXOProvider(BTC btc, Wallet wallet, KeySearchMode keySearchMode) {
this.btc = btc;
this.wallet = wallet;
this.keySearchMode = keySearchMode;
this.keyChain = this.wallet.getActiveKeyChain();
// Set up wallet's key chain
this.keyChain.setLookaheadSize(LOOKAHEAD_INCREMENT);
this.keyChain.maybeLookAhead();
}
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
List<UTXO> allUnspentOutputs = new ArrayList<>();
final boolean coinbase = false;
int ki = 0;
do {
boolean areAllKeysUnspent = true;
boolean areAllKeysSpent = true;
for (; ki < keys.size(); ++ki) {
ECKey key = keys.get(ki);
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = btc.electrumX.getUnspentOutputs(script);
if (unspentOutputs == null)
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
/*
* If there are no unspent outputs then either:
* a) all the outputs have been spent
* b) address has never been used
*
* For case (a) we want to remember not to check this address (key) again.
*/
if (unspentOutputs.isEmpty()) {
// If this is a known key that has been spent before, then we can skip asking for transaction history
if (btc.spentKeys.contains(key)) {
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false;
continue;
}
// Ask for transaction history - if it's empty then key has never been used
List<byte[]> historicTransactionHashes = btc.electrumX.getAddressTransactions(script);
if (historicTransactionHashes == null)
throw new UTXOProviderException(
String.format("Unable to fetch transaction history for %s", address));
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
btc.spentKeys.add(key);
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false;
} else {
// Key never been used - case (b)
areAllKeysSpent = false;
}
continue;
}
// If we reach here, then there's definitely at least one unspent key
areAllKeysSpent = false;
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = btc.getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
HashCode.fromBytes(unspentOutput.hash)));
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
UTXO utxo = new UTXO(Sha256Hash.wrap(unspentOutput.hash), unspentOutput.index,
Coin.valueOf(unspentOutput.value), unspentOutput.height, coinbase,
transactionOutput.getScriptPubKey());
allUnspentOutputs.add(utxo);
}
}
if ((this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ALL_SPENT && areAllKeysSpent)
|| (this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ANY_SPENT && !areAllKeysUnspent)) {
// Generate some more keys
this.keyChain.setLookaheadSize(this.keyChain.getLookaheadSize() + LOOKAHEAD_INCREMENT);
this.keyChain.maybeLookAhead();
// This returns all keys, including those already in 'keys'
List<DeterministicKey> allLeafKeys = this.keyChain.getLeafKeys();
// Add only new keys onto our list of keys to search
List<DeterministicKey> newKeys = allLeafKeys.subList(ki, allLeafKeys.size());
keys.addAll(newKeys);
// Fall-through to checking more keys as now 'ki' is smaller than 'keys.size()' again
}
// If we have processed all keys, then we're done
} while (ki < keys.size());
return allUnspentOutputs;
}
public int getChainHeadHeight() throws UTXOProviderException {
Integer height = btc.electrumX.getCurrentHeight();
if (height == null)
throw new UTXOProviderException("Unable to determine Bitcoin chain height");
return height.intValue();
}
public NetworkParameters getParams() {
return btc.params;
}
}
// Utility methods for us
private byte[] addressToScript(String base58Address) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,236 @@
package org.qortal.crosschain;
import java.util.List;
import java.util.function.Function;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Transaction.SigHash;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
import org.qortal.crypto.Crypto;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
public class BTCP2SH {
public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000;
/*
* OP_TUCK (to copy public key to before signature)
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
* OP_HASH160 (convert public key to PKH)
* OP_DUP (duplicate PKH)
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
* OP_IF
* OP_DROP (no need for duplicate PKH)
* <push 4 bytes> <locktime>
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
* OP_ELSE
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
* OP_HASH160 (hash secret)
* <push 20 bytes> <hash of secret> OP_EQUAL (do hashes of secrets match? if true, script passes else script fails)
* OP_ENDIF
*/
private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes)
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
/**
* Returns Bitcoin redeemScript used for cross-chain trading.
* <p>
* See comments in {@link BTCP2SH} for more details.
*
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
* @return
*/
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5);
}
/**
* Builds a custom transaction to spend P2SH.
*
* @param amount output amount, should be total of input amounts, less miner fees
* @param spendKey key for signing transaction, and also where funds are 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param lockTime (optional) transaction nLockTime, used in refund scenario
* @param scriptSigBuilder function for building scriptSig using transaction input signature
* @param outputPublicKeyHash PKH used to create P2PKH output
* @return Signed Bitcoin transaction for spending P2SH
*/
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes,
Long lockTime, Function<byte[], Script> scriptSigBuilder, byte[] outputPublicKeyHash) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
Transaction transaction = new Transaction(params);
transaction.setVersion(2);
// Output is back to P2SH funder
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(outputPublicKeyHash));
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
// Input (without scriptSig prior to signing)
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
if (lockTime != null)
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
else
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
transaction.addInput(input);
}
// Set locktime after inputs added but before input signatures are generated
if (lockTime != null)
transaction.setLockTime(lockTime);
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
// Generate transaction signature for input
final boolean anyoneCanPay = false;
TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
// Calculate transaction signature
byte[] txSigBytes = txSig.encodeToBitcoin();
// Build scriptSig using lambda and tx signature
Script scriptSig = scriptSigBuilder.apply(txSigBytes);
// Set input scriptSig
transaction.getInput(inputIndex).setScriptSig(scriptSig);
}
return transaction;
}
/**
* Returns signed Bitcoin transaction claiming refund from P2SH address.
*
* @param refundAmount refund amount, should be total of input amounts, less miner fees
* @param refundKey key for signing transaction, and also where refund is 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
* @return Signed Bitcoin transaction for refunding P2SH
*/
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
// transaction signature
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
// redeem public key
byte[] refundPubKey = refundKey.getPubKey();
scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey));
// redeem script
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
return scriptBuilder.build();
};
// Send funds back to funding address
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, refundKey.getPubKeyHash());
}
/**
* Returns signed Bitcoin transaction redeeming funds from P2SH address.
*
* @param redeemAmount redeem amount, should be total of input amounts, less miner fees
* @param redeemKey key for signing transaction, and also where funds are 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param secret actual 32-byte secret used when building redeemScript
* @param receivingAccountInfo Bitcoin PKH used for output
* @return Signed Bitcoin transaction for redeeming P2SH
*/
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) {
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
// secret
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
// transaction signature
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
// redeem public key
byte[] redeemPubKey = redeemKey.getPubKey();
scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey));
// redeem script
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
return scriptBuilder.build();
};
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo);
}
/** Returns 'secret', if any, given list of raw bitcoin transactions. */
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
for (byte[] rawTransaction : rawTransactions) {
Transaction transaction = new Transaction(params, rawTransaction);
// Cycle through inputs, looking for one that spends our P2SH
for (TransactionInput input : transaction.getInputs()) {
Script scriptSig = input.getScriptSig();
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
// Expected number of script chunks for redeem. Refund might not have the same number.
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
if (scriptChunks.size() != expectedChunkCount)
continue;
// We're expecting last chunk to contain the actual redeemScript
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
byte[] redeemScriptBytes = lastChunk.data;
// If non-push scripts, redeemScript will be null
if (redeemScriptBytes == null)
continue;
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!inputAddress.toString().equals(p2shAddress))
// Input isn't spending our P2SH
continue;
byte[] secret = scriptChunks.get(0).data;
if (secret.length != BTCP2SH.SECRET_LENGTH)
continue;
return secret;
}
}
return null;
}
}

View File

@@ -25,11 +25,11 @@ import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.TrustlessSSLSocketFactory;
import org.qortal.utils.Pair;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
/** ElectrumX network support for querying Bitcoin-related info like block headers, transaction outputs, etc. */
public class ElectrumX {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
@@ -93,7 +93,21 @@ public class ElectrumX {
private ElectrumX(String bitcoinNetwork) {
switch (bitcoinNetwork) {
case "MAIN":
servers.addAll(Arrays.asList());
servers.addAll(Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("tardis.bauerj.eu", Server.ConnectionType.SSL, 50002),
new Server("rbx.curalle.ovh", Server.ConnectionType.SSL, 50002),
new Server("quick.electumx.live", Server.ConnectionType.SSL, 50002),
new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002),
new Server("electrumx.ddns.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx.ml", Server.ConnectionType.SSL, 50002),
new Server("electrum.eff.ro", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512),
new Server("E-X.not.fyi", Server.ConnectionType.SSL, 50002),
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 50001),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001)));
break;
case "TEST3":
@@ -119,6 +133,7 @@ public class ElectrumX {
rpc("server.banner");
}
/** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */
public static synchronized ElectrumX getInstance(String bitcoinNetwork) {
if (!instances.containsKey(bitcoinNetwork))
instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork));
@@ -129,16 +144,26 @@ public class ElectrumX {
// Methods for use by other classes
public Integer getCurrentHeight() {
JSONObject blockJson = (JSONObject) this.rpc("blockchain.headers.subscribe");
if (blockJson == null || !blockJson.containsKey("height"))
Object blockObj = this.rpc("blockchain.headers.subscribe");
if (!(blockObj instanceof JSONObject))
return null;
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("height"))
return null;
return ((Long) blockJson.get("height")).intValue();
}
public List<byte[]> getBlockHeaders(int startHeight, long count) {
JSONObject blockJson = (JSONObject) this.rpc("blockchain.block.headers", startHeight, count);
if (blockJson == null || !blockJson.containsKey("count") || !blockJson.containsKey("hex"))
Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
if (!(blockObj instanceof JSONObject))
return null;
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("count") || !blockJson.containsKey("hex"))
return null;
Long returnedCount = (Long) blockJson.get("count");
@@ -155,57 +180,87 @@ public class ElectrumX {
return rawBlockHeaders;
}
/** Returns confirmed balance, based on passed payment script, or null if there was an error or no known balance. */
public Long getBalance(byte[] script) {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
JSONObject balanceJson = (JSONObject) this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (balanceJson == null || !balanceJson.containsKey("confirmed"))
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (!(balanceObj instanceof JSONObject))
return null;
JSONObject balanceJson = (JSONObject) balanceObj;
if (!balanceJson.containsKey("confirmed"))
return null;
return (Long) balanceJson.get("confirmed");
}
public List<Pair<byte[], Integer>> getUnspentOutputs(byte[] script) {
/** Unspent output info as returned by ElectrumX network. */
public static class UnspentOutput {
public final byte[] hash;
public final int index;
public final int height;
public final long value;
public UnspentOutput(byte[] hash, int index, int height, long value) {
this.hash = hash;
this.index = index;
this.height = height;
this.value = value;
}
}
/** Returns list of unspent outputs pertaining to passed payment script, or null if there was an error. */
public List<UnspentOutput> getUnspentOutputs(byte[] script) {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
JSONArray unspentJson = (JSONArray) this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (unspentJson == null)
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (!(unspentJson instanceof JSONArray))
return null;
List<Pair<byte[], Integer>> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : unspentJson) {
List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : (JSONArray) unspentJson) {
JSONObject unspent = (JSONObject) rawUnspent;
int height = ((Long) unspent.get("height")).intValue();
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
if (height <= 0)
continue;
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
int outputIndex = ((Long) unspent.get("tx_pos")).intValue();
long value = (Long) unspent.get("value");
unspentOutputs.add(new Pair<>(txHash, outputIndex));
unspentOutputs.add(new UnspentOutput(txHash, outputIndex, height, value));
}
return unspentOutputs;
}
/** Returns raw transaction for passed transaction hash, or null if not found. */
public byte[] getRawTransaction(byte[] txHash) {
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
if (rawTransactionHex == null)
Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
if (!(rawTransactionHex instanceof String))
return null;
return HashCode.fromString(rawTransactionHex).asBytes();
return HashCode.fromString((String) rawTransactionHex).asBytes();
}
/** Returns list of raw transactions, relating to passed payment script, if null if there's an error. */
public List<byte[]> getAddressTransactions(byte[] script) {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
JSONArray transactionsJson = (JSONArray) this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (transactionsJson == null)
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (!(transactionsJson instanceof JSONArray))
return null;
List<byte[]> rawTransactions = new ArrayList<>();
for (Object rawTransactionInfo : transactionsJson) {
for (Object rawTransactionInfo : (JSONArray) transactionsJson) {
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
// We only want confirmed transactions
@@ -223,6 +278,7 @@ public class ElectrumX {
return rawTransactions;
}
/** Returns true if raw transaction successfully broadcast. */
public boolean broadcastTransaction(byte[] transactionBytes) {
Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
if (rawBroadcastResult == null)
@@ -235,14 +291,15 @@ public class ElectrumX {
// Class-private utility methods
/** Query current server for its list of peer servers, and return those we can parse. */
private Set<Server> serverPeersSubscribe() {
Set<Server> newServers = new HashSet<>();
JSONArray peers = (JSONArray) this.connectedRpc("server.peers.subscribe");
if (peers == null)
Object peers = this.connectedRpc("server.peers.subscribe");
if (!(peers instanceof JSONArray))
return newServers;
for (Object rawPeer : peers) {
for (Object rawPeer : (JSONArray) peers) {
JSONArray peer = (JSONArray) rawPeer;
if (peer.size() < 3)
continue;
@@ -287,6 +344,7 @@ public class ElectrumX {
return newServers;
}
/** Return output from RPC call, with automatic reconnection to different server if needed. */
private synchronized Object rpc(String method, Object...params) {
while (haveConnection()) {
Object response = connectedRpc(method, params);
@@ -305,6 +363,7 @@ public class ElectrumX {
return null;
}
/** Returns true if we have, or create, a connection to an ElectrumX server. */
private boolean haveConnection() {
if (this.currentServer != null)
return true;
@@ -377,10 +436,12 @@ public class ElectrumX {
if (response.isEmpty())
return null;
JSONObject responseJson = (JSONObject) JSONValue.parse(response);
if (responseJson == null)
Object responseObj = JSONValue.parse(response);
if (!(responseObj instanceof JSONObject))
return null;
JSONObject responseJson = (JSONObject) responseObj;
return responseJson.get("result");
}

View File

@@ -1,11 +1,15 @@
package org.qortal.data.account;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.utils.Base58;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@@ -71,4 +75,13 @@ public class RewardShareData {
return this.minter;
}
// For debugging
public String toString() {
if (this.minter.equals(this.recipient))
return String.format("Minter/recipient: %s, reward-share public key: %s", this.minter, Base58.encode(this.rewardSharePublicKey));
else
return String.format("Minter: %s, recipient: %s (%s %%), reward-share public key: %s", this.minter, this.recipient, BigDecimal.valueOf(this.sharePercent, 2), Base58.encode(this.rewardSharePublicKey));
}
}

View File

@@ -4,14 +4,14 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.BTCACCT;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeData {
public enum Mode { OFFER, TRADE };
// Properties
@Schema(description = "AT's Qortal address")
@@ -20,32 +20,40 @@ public class CrossChainTradeData {
@Schema(description = "AT creator's Qortal address")
public String qortalCreator;
@Schema(description = "AT creator's Qortal trade address")
public String qortalCreatorTradeAddress;
@Schema(description = "AT creator's Bitcoin trade public-key-hash (PKH)")
public byte[] creatorBitcoinPKH;
@Schema(description = "Timestamp when AT was created (milliseconds since epoch)")
public long creationTimestamp;
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
public int tradeTimeout;
@Schema(description = "AT's current QORT balance")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long qortBalance;
@Schema(description = "HASH160 of 32-byte secret")
public byte[] secretHash;
@Schema(description = "HASH160 of 32-byte secret-A")
public byte[] hashOfSecretA;
@Schema(description = "Initial QORT payment that will be sent to Qortal trade partner")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long initialPayout;
@Schema(description = "HASH160 of 32-byte secret-B")
public byte[] hashOfSecretB;
@Schema(description = "Final QORT payment that will be sent to Qortal trade partner")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long redeemPayout;
public long qortAmount;
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
public String qortalRecipient;
public String qortalPartnerAddress;
@Schema(description = "Timestamp when AT switched to trade mode")
public Long tradeModeTimestamp;
@Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)")
public long tradeRefundTimeout;
@Schema(description = "How long from AT creation until AT triggers automatic refund to AT creator (minutes)")
public Integer refundTimeout;
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
public Integer tradeRefundHeight;
@@ -54,10 +62,19 @@ public class CrossChainTradeData {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long expectedBitcoin;
public Mode mode;
public BTCACCT.Mode mode;
@Schema(description = "Suggested Bitcoin P2SH nLockTime based on trade timeout")
public Integer lockTime;
@Schema(description = "Suggested Bitcoin P2SH-A nLockTime based on trade timeout")
public Integer lockTimeA;
@Schema(description = "Suggested Bitcoin P2SH-B nLockTime based on trade timeout")
public Integer lockTimeB;
@Schema(description = "Trade partner's Bitcoin public-key-hash (PKH)")
public byte[] partnerBitcoinPKH;
@Schema(description = "Trade partner's Qortal receiving address")
public String qortalPartnerReceivingAddress;
// Constructors

View File

@@ -0,0 +1,193 @@
package org.qortal.data.crosschain;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotData {
private byte[] tradePrivateKey;
public enum State {
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(15), BOB_WAITING_FOR_P2SH_B(20), BOB_WAITING_FOR_AT_REDEEM(25), BOB_DONE(30), BOB_REFUNDED(35),
ALICE_WAITING_FOR_P2SH_A(80), ALICE_WAITING_FOR_AT_LOCK(85), ALICE_WATCH_P2SH_B(90), ALICE_DONE(95), ALICE_REFUNDING_B(100), ALICE_REFUNDING_A(105), ALICE_REFUNDED(110);
public final int value;
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
State(int value) {
this.value = value;
}
public static State valueOf(int value) {
return map.get(value);
}
}
private State tradeState;
private String creatorAddress;
private String atAddress;
private long timestamp;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long qortAmount;
private byte[] tradeNativePublicKey;
private byte[] tradeNativePublicKeyHash;
String tradeNativeAddress;
private byte[] secret;
private byte[] hashOfSecret;
private byte[] tradeForeignPublicKey;
private byte[] tradeForeignPublicKeyHash;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long bitcoinAmount;
// Never expose this via API
@XmlTransient
@Schema(hidden = true)
private String xprv58;
private byte[] lastTransactionSignature;
private Integer lockTimeA;
// Could be Bitcoin or Qortal...
private byte[] receivingAccountInfo;
protected TradeBotData() {
/* JAXB */
}
public TradeBotData(byte[] tradePrivateKey, State tradeState, String creatorAddress, String atAddress,
long timestamp, long qortAmount,
byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, String tradeNativeAddress,
byte[] secret, byte[] hashOfSecret,
byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash,
long bitcoinAmount, String xprv58, byte[] lastTransactionSignature, Integer lockTimeA, byte[] receivingAccountInfo) {
this.tradePrivateKey = tradePrivateKey;
this.tradeState = tradeState;
this.creatorAddress = creatorAddress;
this.atAddress = atAddress;
this.timestamp = timestamp;
this.qortAmount = qortAmount;
this.tradeNativePublicKey = tradeNativePublicKey;
this.tradeNativePublicKeyHash = tradeNativePublicKeyHash;
this.tradeNativeAddress = tradeNativeAddress;
this.secret = secret;
this.hashOfSecret = hashOfSecret;
this.tradeForeignPublicKey = tradeForeignPublicKey;
this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash;
this.bitcoinAmount = bitcoinAmount;
this.xprv58 = xprv58;
this.lastTransactionSignature = lastTransactionSignature;
this.lockTimeA = lockTimeA;
this.receivingAccountInfo = receivingAccountInfo;
}
public byte[] getTradePrivateKey() {
return this.tradePrivateKey;
}
public State getState() {
return this.tradeState;
}
public void setState(State state) {
this.tradeState = state;
}
public String getCreatorAddress() {
return this.creatorAddress;
}
public String getAtAddress() {
return this.atAddress;
}
public void setAtAddress(String atAddress) {
this.atAddress = atAddress;
}
public long getTimestamp() {
return this.timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public long getQortAmount() {
return this.qortAmount;
}
public byte[] getTradeNativePublicKey() {
return this.tradeNativePublicKey;
}
public byte[] getTradeNativePublicKeyHash() {
return this.tradeNativePublicKeyHash;
}
public String getTradeNativeAddress() {
return this.tradeNativeAddress;
}
public byte[] getSecret() {
return this.secret;
}
public byte[] getHashOfSecret() {
return this.hashOfSecret;
}
public byte[] getTradeForeignPublicKey() {
return this.tradeForeignPublicKey;
}
public byte[] getTradeForeignPublicKeyHash() {
return this.tradeForeignPublicKeyHash;
}
public long getBitcoinAmount() {
return this.bitcoinAmount;
}
public String getXprv58() {
return this.xprv58;
}
public byte[] getLastTransactionSignature() {
return this.lastTransactionSignature;
}
public void setLastTransactionSignature(byte[] lastTransactionSignature) {
this.lastTransactionSignature = lastTransactionSignature;
}
public Integer getLockTimeA() {
return this.lockTimeA;
}
public void setLockTimeA(Integer lockTimeA) {
this.lockTimeA = lockTimeA;
}
public byte[] getReceivingAccountInfo() {
return this.receivingAccountInfo;
}
}

View File

@@ -0,0 +1,5 @@
package org.qortal.event;
public interface Event {
}

View File

@@ -0,0 +1,33 @@
package org.qortal.event;
import java.util.ArrayList;
import java.util.List;
public enum EventBus {
INSTANCE;
private static final List<Listener> LISTENERS = new ArrayList<>();
public void addListener(Listener newListener) {
synchronized (LISTENERS) {
LISTENERS.add(newListener);
}
}
public void removeListener(Listener listener) {
synchronized (LISTENERS) {
LISTENERS.remove(listener);
}
}
public void notify(Event event) {
List<Listener> clonedListeners;
synchronized (LISTENERS) {
clonedListeners = new ArrayList<>(LISTENERS);
}
for (Listener listener : clonedListeners)
listener.listen(event);
}
}

View File

@@ -0,0 +1,6 @@
package org.qortal.event;
@FunctionalInterface
public interface Listener {
void listen(Event event);
}

View File

@@ -1,6 +1,8 @@
package org.qortal.network;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -13,7 +15,9 @@ import org.qortal.network.message.ChallengeMessage;
import org.qortal.network.message.HelloMessage;
import org.qortal.network.message.Message;
import org.qortal.network.message.Message.MessageType;
import org.qortal.settings.Settings;
import org.qortal.network.message.ResponseMessage;
import org.qortal.utils.DaemonThreadFactory;
import org.qortal.utils.NTP;
import com.google.common.primitives.Bytes;
@@ -27,6 +31,7 @@ public enum Handshake {
@Override
public void action(Peer peer) {
/* Never called */
}
},
HELLO(MessageType.HELLO) {
@@ -157,13 +162,20 @@ public enum Handshake {
}
int nonce = responseMessage.getNonce();
if (!MemoryPoW.verify2(data, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce)) {
int powBufferSize = peer.getPeersVersion() < PEER_VERSION_131 ? POW_BUFFER_SIZE_PRE_131 : POW_BUFFER_SIZE_POST_131;
int powDifficulty = peer.getPeersVersion() < PEER_VERSION_131 ? POW_DIFFICULTY_PRE_131 : POW_DIFFICULTY_POST_131;
if (!MemoryPoW.verify2(data, powBufferSize, powDifficulty, nonce)) {
LOGGER.debug(() -> String.format("Peer %s sent incorrect RESPONSE nonce", peer));
return null;
}
peer.setPeersNodeId(Crypto.toNodeAddress(peersPublicKey));
// For inbound peers, we need to go into interim holding state while we compute RESPONSE
if (!peer.isOutbound())
return RESPONDING;
// Handshake completed!
return COMPLETED;
}
@@ -178,28 +190,52 @@ public enum Handshake {
final byte[] data = Crypto.digest(Bytes.concat(sharedSecret, peersChallenge));
// We do this in a new thread as it can take a while...
Thread responseThread = new Thread(() -> {
Integer nonce = MemoryPoW.compute2(data, POW_BUFFER_SIZE, POW_DIFFICULTY);
responseExecutor.execute(() -> {
// Are we still connected?
if (peer.isStopping())
// No point computing for dead peer
return;
int powBufferSize = peer.getPeersVersion() < PEER_VERSION_131 ? POW_BUFFER_SIZE_PRE_131 : POW_BUFFER_SIZE_POST_131;
int powDifficulty = peer.getPeersVersion() < PEER_VERSION_131 ? POW_DIFFICULTY_PRE_131 : POW_DIFFICULTY_POST_131;
Integer nonce = MemoryPoW.compute2(data, powBufferSize, powDifficulty);
Message responseMessage = new ResponseMessage(nonce, data);
if (!peer.sendMessage(responseMessage))
peer.disconnect("failed to send RESPONSE");
});
responseThread.setDaemon(true);
responseThread.start();
// For inbound peers, we should actually be in RESPONDING state.
// So we need to do the extra work to move to COMPLETED state.
if (!peer.isOutbound()) {
peer.setHandshakeStatus(COMPLETED);
Network.getInstance().onHandshakeCompleted(peer);
}
});
}
},
COMPLETED(null) {
// Interim holding state while we compute RESPONSE to send to inbound peer
RESPONDING(null) {
@Override
public Handshake onMessage(Peer peer, Message message) {
// Handshake completed
// Should never be called
return null;
}
@Override
public void action(Peer peer) {
// Note: this is only called when we've made outbound connection
// Should never be called
}
},
COMPLETED(null) {
@Override
public Handshake onMessage(Peer peer, Message message) {
// Should never be called
return null;
}
@Override
public void action(Peer peer) {
// Note: this is only called if we've made outbound connection
}
};
@@ -210,8 +246,16 @@ public enum Handshake {
private static final Pattern VERSION_PATTERN = Pattern.compile(Controller.VERSION_PREFIX + "(\\d{1,3})\\.(\\d{1,5})\\.(\\d{1,5})");
private static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
private static final int POW_DIFFICULTY = 8; // leading zero bits
private static final long PEER_VERSION_131 = 0x0100030001L;
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
// Can always be made harder in the future...
private static final int POW_BUFFER_SIZE_POST_131 = 2 * 1024 * 1024; // bytes
private static final int POW_DIFFICULTY_POST_131 = 2; // leading zero bits
private static final ExecutorService responseExecutor = Executors.newFixedThreadPool(Settings.getInstance().getNetworkPoWComputePoolSize(), new DaemonThreadFactory("Network-PoW"));
private static final byte[] ZERO_CHALLENGE = new byte[ChallengeMessage.CHALLENGE_LENGTH];

View File

@@ -55,6 +55,7 @@ import org.qortal.utils.ExecuteProduceConsume;
// import org.qortal.utils.ExecutorDumper;
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
import org.qortal.utils.NTP;
import org.qortal.utils.NamedThreadFactory;
// For managing peers
public class Network {
@@ -80,16 +81,9 @@ public class Network {
private static final byte[] TESTNET_MESSAGE_MAGIC = new byte[] { 0x71, 0x6f, 0x72, 0x54 }; // qorT
private static final String[] INITIAL_PEERS = new String[] {
"node1.qortal.org",
"node2.qortal.org",
"node3.qortal.org",
"node4.qortal.org",
"node5.qortal.org",
"node6.qortal.org",
"node7.qortal.org",
"node8.qortal.org",
"node9.qortal.org",
"node10.qortal.org"
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
"node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org",
"node.qortal.ru", "node2.qortal.ru", "node3.qortal.ru", "node.qortal.uk"
};
private static final long NETWORK_EPC_KEEPALIVE = 10L; // seconds
@@ -151,7 +145,8 @@ public class Network {
ExecutorService networkExecutor = new ThreadPoolExecutor(1,
Settings.getInstance().getMaxNetworkThreadPoolSize(),
NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
new SynchronousQueue<Runnable>(),
new NamedThreadFactory("Network-EPC"));
networkEPC = new NetworkProcessor(networkExecutor);
}
@@ -355,7 +350,7 @@ public class Network {
private Task maybeProducePeerPingTask(Long now) {
// Ask connected peers whether they need a ping
for (Peer peer : getConnectedPeers()) {
for (Peer peer : getHandshakedPeers()) {
Task peerTask = peer.getPingTask(now);
if (peerTask != null)
return peerTask;
@@ -635,7 +630,7 @@ public class Network {
/** Called when Peer's thread has setup and is ready to process messages */
public void onPeerReady(Peer peer) {
this.onMessage(peer, null);
onHandshakingMessage(peer, null, Handshake.STARTED);
}
public void onDisconnect(Peer peer) {
@@ -777,7 +772,7 @@ public class Network {
opportunisticMergePeers(peer.toString(), peerV2Addresses);
}
private void onHandshakeCompleted(Peer peer) {
/*pacakge*/ void onHandshakeCompleted(Peer peer) {
LOGGER.debug(String.format("Handshake completed with peer %s", peer));
// Are we already connected to this peer?

View File

@@ -372,9 +372,11 @@ public class Peer {
if (message == null && bytesRead == 0 && !wasByteBufferFull) {
// No complete message in buffer, no more bytes to read from socket even though there was room to read bytes
/* DISABLED
// If byteBuffer is empty then we can deallocate it, to save memory, albeit costing GC
if (this.byteBuffer.remaining() == this.byteBuffer.capacity())
this.byteBuffer = null;
*/
return;
}

View File

@@ -15,6 +15,9 @@ public interface ATRepository {
/** Returns where AT with passed address exists in repository */
public boolean exists(String atAddress) throws DataException;
/** Returns AT creator's public key, or null if not found */
public byte[] getCreatorPublicKey(String atAddress) throws DataException;
/** Returns list of executable ATs, empty if none found */
public List<ATData> getAllExecutableATs() throws DataException;
@@ -54,6 +57,24 @@ public interface ATRepository {
*/
public ATStateData getLatestATState(String atAddress) throws DataException;
/**
* Returns final ATStateData for ATs matching codeHash (required)
* and specific data segment value (optional).
* <p>
* If searching for specific data segment value, both <tt>dataByteOffset</tt>
* and <tt>expectedValue</tt> need to be non-null.
* <p>
* Note that <tt>dataByteOffset</tt> starts from 0 and will typically be
* a multiple of <tt>MachineState.VALUE_SIZE</tt>, which is usually 8:
* width of a long.
* <p>
* Although <tt>expectedValue</tt>, if provided, is natively an unsigned long,
* the data segment comparison is done via unsigned hex string.
*/
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Returns all ATStateData for a given block height.
* <p>
@@ -88,4 +109,28 @@ public interface ATRepository {
/** Delete state data for all ATs at this height */
public void deleteATStates(int height) throws DataException;
// Finding transactions for ATs to process
static class NextTransactionInfo {
public final int height;
public final int sequence;
public final byte[] signature;
public NextTransactionInfo(int height, int sequence, byte[] signature) {
this.height = height;
this.sequence = sequence;
this.signature = signature;
}
}
/**
* Find next transaction for AT to process.
* <p>
* @param recipient AT address
* @param height starting height
* @param sequence starting sequence
* @return next transaction info, or null if none found
*/
public NextTransactionInfo findNextTransaction(String recipient, int height, int sequence) throws DataException;
}

View File

@@ -169,8 +169,8 @@ public interface AccountRepository {
public void save(MintingAccountData mintingAccountData) throws DataException;
/** Delete minting account info, used by BlockMinter, from repository using passed private key. */
public int delete(byte[] mintingAccountPrivateKey) throws DataException;
/** Delete minting account info, used by BlockMinter, from repository using passed public or private key. */
public int delete(byte[] mintingAccountKey) throws DataException;
// Managing QORT from legacy QORA

View File

@@ -2,6 +2,7 @@ package org.qortal.repository;
import java.util.List;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
@@ -60,6 +61,15 @@ public interface BlockRepository {
*/
public int getHeightFromTimestamp(long timestamp) throws DataException;
/**
* Returns block timestamp for a given height.
*
* @param height
* @return timestamp, or 0 if height is out of bounds.
* @throws DataException
*/
public long getTimestampFromHeight(int height) throws DataException;
/**
* Return highest block height from repository.
*
@@ -128,6 +138,11 @@ public interface BlockRepository {
*/
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException;
/**
* Returns block infos for the passed height range, for API use.
*/
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
/**
* Trim online accounts signatures from blocks older than passed timestamp.
*
@@ -137,11 +152,12 @@ public interface BlockRepository {
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException;
/**
* Returns first (lowest height) block that doesn't link back to genesis block.
* Returns first (lowest height) block that doesn't link back to specified block.
*
* @param startHeight height of specified block
* @throws DataException
*/
public BlockData getDetachedBlockSignature() throws DataException;
public BlockData getDetachedBlockSignature(int startHeight) throws DataException;
/**
* Saves block into repository.

View File

@@ -0,0 +1,18 @@
package org.qortal.repository;
import java.util.List;
import org.qortal.data.crosschain.TradeBotData;
public interface CrossChainRepository {
public TradeBotData getTradeBotData(byte[] tradePrivateKey) throws DataException;
public List<TradeBotData> getAllTradeBotData() throws DataException;
public void save(TradeBotData tradeBotData) throws DataException;
/** Delete trade-bot states using passed private key. */
public int delete(byte[] tradePrivateKey) throws DataException;
}

View File

@@ -14,6 +14,8 @@ public interface Repository extends AutoCloseable {
public ChatRepository getChatRepository();
public CrossChainRepository getCrossChainRepository();
public GroupRepository getGroupRepository();
public NameRepository getNameRepository();

View File

@@ -6,6 +6,7 @@ import java.util.Map;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.data.group.GroupApprovalData;
import org.qortal.data.transaction.GroupApprovalTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.transaction.Transaction.TransactionType;
@@ -107,6 +108,18 @@ public interface TransactionRepository {
*/
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
/**
* Returns list of MESSAGE transaction data matching recipient.
* @param recipient
* @param limit
* @param offset
* @param reverse
* @return
* @throws DataException
*/
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Returns list of transactions relating to specific asset ID.
*

View File

@@ -68,6 +68,20 @@ public class HSQLDBATRepository implements ATRepository {
}
}
@Override
public byte[] getCreatorPublicKey(String atAddress) throws DataException {
String sql = "SELECT creator FROM ATs WHERE AT_address = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
if (resultSet == null)
return null;
return resultSet.getBytes(1);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT creator's public key from repository", e);
}
}
@Override
public List<ATData> getAllExecutableATs() throws DataException {
String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, "
@@ -254,7 +268,8 @@ public class HSQLDBATRepository implements ATRepository {
+ "FROM ATStates "
+ "WHERE AT_address = ? "
+ "ORDER BY height DESC "
+ "LIMIT 1";
+ "LIMIT 1 "
+ "USING INDEX";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
if (resultSet == null)
@@ -273,6 +288,79 @@ public class HSQLDBATRepository implements ATRepository {
}
}
@Override
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT AT_address, height, created_when, state_data, state_hash, fees, is_initial "
+ "FROM ATs "
+ "CROSS JOIN LATERAL("
+ "SELECT height, created_when, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE ATStates.AT_address = ATs.AT_address "
+ "ORDER BY height DESC "
+ "LIMIT 1 "
+ "USING INDEX"
+ ") AS FinalATStates "
+ "WHERE code_hash = ? ");
List<Object> bindParams = new ArrayList<>();
bindParams.add(codeHash);
if (isFinished != null) {
sql.append("AND is_finished = ?");
bindParams.add(isFinished);
}
if (dataByteOffset != null && expectedValue != null) {
sql.append("AND RAWTOHEX(SUBSTRING(state_data FROM ? FOR 8)) = ? ");
// We convert our long to hex Java-side to control endian
String expectedHexValue = String.format("%016x", expectedValue); // left-zero-padding and conversion
// SQL binary data offsets start at 1
bindParams.add(dataByteOffset + 1);
bindParams.add(expectedHexValue);
}
if (minimumFinalHeight != null) {
sql.append("AND height >= ");
sql.append(minimumFinalHeight);
}
sql.append(" ORDER BY height ");
if (reverse != null && reverse)
sql.append("DESC");
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
List<ATStateData> atStates = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return atStates;
do {
String atAddress = resultSet.getString(1);
int height = resultSet.getInt(2);
long created = resultSet.getLong(3);
byte[] stateData = resultSet.getBytes(4); // Actually BLOB
byte[] stateHash = resultSet.getBytes(5);
long fees = resultSet.getLong(6);
boolean isInitial = resultSet.getBoolean(7);
ATStateData atStateData = new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
atStates.add(atStateData);
} while (resultSet.next());
return atStates;
} catch (SQLException e) {
throw new DataException("Unable to fetch matching AT states from repository", e);
}
}
@Override
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
String sql = "SELECT AT_address, state_hash, fees, is_initial "
@@ -341,4 +429,40 @@ public class HSQLDBATRepository implements ATRepository {
}
}
// Finding transactions for ATs to process
public NextTransactionInfo findNextTransaction(String recipient, int height, int sequence) throws DataException {
// We only need to search for a subset of transaction types: MESSAGE, PAYMENT or AT
String sql = "SELECT height, sequence, Transactions.signature "
+ "FROM ("
+ "SELECT signature FROM PaymentTransactions WHERE recipient = ? "
+ "UNION "
+ "SELECT signature FROM MessageTransactions WHERE recipient = ? "
+ "UNION "
+ "SELECT signature FROM ATTransactions WHERE recipient = ?"
+ ") AS Transactions "
+ "JOIN BlockTransactions ON BlockTransactions.transaction_signature = Transactions.signature "
+ "JOIN Blocks ON Blocks.signature = BlockTransactions.block_signature "
+ "WHERE (height > ? OR (height = ? AND sequence > ?)) "
+ "ORDER BY height ASC, sequence ASC "
+ "LIMIT 1";
Object[] bindParams = new Object[] { recipient, recipient, recipient, height, height, sequence };
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams)) {
if (resultSet == null)
return null;
int nextHeight = resultSet.getInt(1);
int nextSequence = resultSet.getInt(2);
byte[] nextSignature = resultSet.getBytes(3);
return new NextTransactionInfo(nextHeight, nextSequence, nextSignature);
} catch (SQLException e) {
throw new DataException("Unable to find next transaction to AT from repository", e);
}
}
}

View File

@@ -774,9 +774,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
@Override
public int delete(byte[] minterPrivateKey) throws DataException {
public int delete(byte[] minterKey) throws DataException {
try {
return this.repository.delete("MintingAccounts", "minter_private_key = ?", minterPrivateKey);
return this.repository.delete("MintingAccounts", "minter_private_key = ? OR minter_public_key = ?", minterKey, minterKey);
} catch (SQLException e) {
throw new DataException("Unable to delete minting account from repository", e);
}

View File

@@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
@@ -131,6 +132,20 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public long getTimestampFromHeight(int height) throws DataException {
String sql = "SELECT minted_when FROM Blocks WHERE height = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, height)) {
if (resultSet == null)
return 0;
return resultSet.getLong(1);
} catch (SQLException e) {
throw new DataException("Error obtaining block timestamp by height from repository", e);
}
}
@Override
public int getBlockchainHeight() throws DataException {
String sql = "SELECT height FROM Blocks ORDER BY height DESC LIMIT 1";
@@ -360,6 +375,90 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException {
StringBuilder sql = new StringBuilder(512);
sql.append("SELECT signature, height, minted_when, transaction_count, RewardShares.minter ");
/*
* start end count result
* 10 40 null blocks 10 to 39 (excludes end block, ignore count)
*
* null null null blocks 1 to 50 (assume count=50, maybe start=1)
* 30 null null blocks 30 to 79 (assume count=50)
* 30 null 10 blocks 30 to 39
*
* null null 50 last 50 blocks? so if max(blocks.height) is 200, then blocks 151 to 200
* null 200 null blocks 150 to 199 (excludes end block, assume count=50)
* null 200 10 blocks 190 to 199 (excludes end block)
*/
if (startHeight != null && endHeight != null) {
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ");
sql.append(startHeight);
sql.append(" AND ");
sql.append(endHeight - 1);
} else if (endHeight != null || (startHeight == null && count != null)) {
// we are going to return blocks from the end of the chain
if (count == null)
count = 50;
if (endHeight == null) {
sql.append("FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS MaxHeights (max_height) ");
sql.append("JOIN Blocks ON height BETWEEN (max_height - ");
sql.append(count);
sql.append(" + 1) AND max_height ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter");
} else {
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ");
sql.append(endHeight - count);
sql.append(" AND ");
sql.append(endHeight - 1);
}
} else {
// we are going to return blocks from the start of the chain
if (startHeight == null)
startHeight = 1;
if (count == null)
count = 50;
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ");
sql.append(startHeight);
sql.append(" AND ");
sql.append(startHeight + count - 1);
}
List<BlockInfo> blockInfos = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
if (resultSet == null)
return blockInfos;
do {
byte[] signature = resultSet.getBytes(1);
int height = resultSet.getInt(2);
long timestamp = resultSet.getLong(3);
int transactionCount = resultSet.getInt(4);
String minterAddress = resultSet.getString(5);
BlockInfo blockInfo = new BlockInfo(signature, height, timestamp, transactionCount, minterAddress);
blockInfos.add(blockInfo);
} while (resultSet.next());
return blockInfos;
} catch (SQLException e) {
throw new DataException("Unable to fetch height-ranged block infos from repository", e);
}
}
@Override
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException {
String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL";
@@ -372,14 +471,14 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
@Override
public BlockData getDetachedBlockSignature() throws DataException {
public BlockData getDetachedBlockSignature(int startHeight) throws DataException {
String sql = "SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks "
+ "LEFT OUTER JOIN Blocks AS ParentBlocks "
+ "ON ParentBlocks.signature = Blocks.reference "
+ "WHERE ParentBlocks.signature IS NULL AND Blocks.height > 1 "
+ "WHERE ParentBlocks.signature IS NULL AND Blocks.height > ? "
+ "ORDER BY Blocks.height ASC LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, startHeight)) {
return getBlockFromResultSet(resultSet);
} catch (SQLException e) {
throw new DataException("Error fetching block by signature from repository", e);

View File

@@ -0,0 +1,165 @@
package org.qortal.repository.hsqldb;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.repository.CrossChainRepository;
import org.qortal.repository.DataException;
public class HSQLDBCrossChainRepository implements CrossChainRepository {
protected HSQLDBRepository repository;
public HSQLDBCrossChainRepository(HSQLDBRepository repository) {
this.repository = repository;
}
@Override
public TradeBotData getTradeBotData(byte[] tradePrivateKey) throws DataException {
String sql = "SELECT trade_state, creator_address, at_address, "
+ "updated_when, qort_amount, "
+ "trade_native_public_key, trade_native_public_key_hash, "
+ "trade_native_address, secret, hash_of_secret, "
+ "trade_foreign_public_key, trade_foreign_public_key_hash, "
+ "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_account_info "
+ "FROM TradeBotStates "
+ "WHERE trade_private_key = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, tradePrivateKey)) {
if (resultSet == null)
return null;
int tradeStateValue = resultSet.getInt(1);
TradeBotData.State tradeState = TradeBotData.State.valueOf(tradeStateValue);
if (tradeState == null)
throw new DataException("Illegal trade-bot trade-state fetched from repository");
String creatorAddress = resultSet.getString(2);
String atAddress = resultSet.getString(3);
long timestamp = resultSet.getLong(4);
long qortAmount = resultSet.getLong(5);
byte[] tradeNativePublicKey = resultSet.getBytes(6);
byte[] tradeNativePublicKeyHash = resultSet.getBytes(7);
String tradeNativeAddress = resultSet.getString(8);
byte[] secret = resultSet.getBytes(9);
byte[] hashOfSecret = resultSet.getBytes(10);
byte[] tradeForeignPublicKey = resultSet.getBytes(11);
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(12);
long bitcoinAmount = resultSet.getLong(13);
String xprv58 = resultSet.getString(14);
byte[] lastTransactionSignature = resultSet.getBytes(15);
Integer lockTimeA = resultSet.getInt(16);
if (lockTimeA == 0 && resultSet.wasNull())
lockTimeA = null;
byte[] receivingAccountInfo = resultSet.getBytes(17);
return new TradeBotData(tradePrivateKey, tradeState,
creatorAddress, atAddress, timestamp, qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secret, hashOfSecret,
tradeForeignPublicKey, tradeForeignPublicKeyHash,
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingAccountInfo);
} catch (SQLException e) {
throw new DataException("Unable to fetch trade-bot trading state from repository", e);
}
}
@Override
public List<TradeBotData> getAllTradeBotData() throws DataException {
String sql = "SELECT trade_private_key, trade_state, creator_address, at_address, "
+ "updated_when, qort_amount, "
+ "trade_native_public_key, trade_native_public_key_hash, "
+ "trade_native_address, secret, hash_of_secret, "
+ "trade_foreign_public_key, trade_foreign_public_key_hash, "
+ "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_account_info "
+ "FROM TradeBotStates";
List<TradeBotData> allTradeBotData = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
if (resultSet == null)
return allTradeBotData;
do {
byte[] tradePrivateKey = resultSet.getBytes(1);
int tradeStateValue = resultSet.getInt(2);
TradeBotData.State tradeState = TradeBotData.State.valueOf(tradeStateValue);
if (tradeState == null)
throw new DataException("Illegal trade-bot trade-state fetched from repository");
String creatorAddress = resultSet.getString(3);
String atAddress = resultSet.getString(4);
long timestamp = resultSet.getLong(5);
long qortAmount = resultSet.getLong(6);
byte[] tradeNativePublicKey = resultSet.getBytes(7);
byte[] tradeNativePublicKeyHash = resultSet.getBytes(8);
String tradeNativeAddress = resultSet.getString(9);
byte[] secret = resultSet.getBytes(10);
byte[] hashOfSecret = resultSet.getBytes(11);
byte[] tradeForeignPublicKey = resultSet.getBytes(12);
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(13);
long bitcoinAmount = resultSet.getLong(14);
String xprv58 = resultSet.getString(15);
byte[] lastTransactionSignature = resultSet.getBytes(16);
Integer lockTimeA = resultSet.getInt(17);
if (lockTimeA == 0 && resultSet.wasNull())
lockTimeA = null;
byte[] receivingAccountInfo = resultSet.getBytes(18);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState,
creatorAddress, atAddress, timestamp, qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secret, hashOfSecret,
tradeForeignPublicKey, tradeForeignPublicKeyHash,
bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingAccountInfo);
allTradeBotData.add(tradeBotData);
} while (resultSet.next());
return allTradeBotData;
} catch (SQLException e) {
throw new DataException("Unable to fetch trade-bot trading states from repository", e);
}
}
@Override
public void save(TradeBotData tradeBotData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("TradeBotStates");
saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey())
.bind("trade_state", tradeBotData.getState().value)
.bind("creator_address", tradeBotData.getCreatorAddress())
.bind("at_address", tradeBotData.getAtAddress())
.bind("updated_when", tradeBotData.getTimestamp())
.bind("qort_amount", tradeBotData.getQortAmount())
.bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey())
.bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash())
.bind("trade_native_address", tradeBotData.getTradeNativeAddress())
.bind("secret", tradeBotData.getSecret()).bind("hash_of_secret", tradeBotData.getHashOfSecret())
.bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey())
.bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash())
.bind("bitcoin_amount", tradeBotData.getBitcoinAmount())
.bind("xprv58", tradeBotData.getXprv58())
.bind("last_transaction_signature", tradeBotData.getLastTransactionSignature())
.bind("locktime_a", tradeBotData.getLockTimeA())
.bind("receiving_account_info", tradeBotData.getReceivingAccountInfo());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save trade bot data into repository", e);
}
}
@Override
public int delete(byte[] tradePrivateKey) throws DataException {
try {
return this.repository.delete("TradeBotStates", "trade_private_key = ?", tradePrivateKey);
} catch (SQLException e) {
throw new DataException("Unable to delete trade-bot states from repository", e);
}
}
}

View File

@@ -95,7 +95,6 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE COLLATION SQL_TEXT_NO_PAD FOR SQL_TEXT FROM SQL_TEXT NO PAD");
stmt.execute("SET FILES SPACE TRUE"); // Enable per-table block space within .data file, useful for CACHED table types
stmt.execute("SET FILES LOB SCALE 1"); // LOB granularity is 1KB
// Slow down log fsync() calls from every 500ms to reduce I/O load
stmt.execute("SET FILES WRITE DELAY 5"); // only fsync() every 5 seconds
@@ -103,15 +102,15 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )");
stmt.execute("CREATE TYPE ArbitraryData AS VARBINARY(256)");
stmt.execute("CREATE TYPE AssetData AS CLOB(400K)");
stmt.execute("CREATE TYPE AssetData AS VARCHAR(400K)");
stmt.execute("CREATE TYPE AssetID AS BIGINT");
stmt.execute("CREATE TYPE AssetName AS VARCHAR(34) COLLATE SQL_TEXT_NO_PAD");
stmt.execute("CREATE TYPE AssetOrderID AS VARBINARY(64)");
stmt.execute("CREATE TYPE ATCode AS BLOB(64K)"); // 16bit * 1
stmt.execute("CREATE TYPE ATCreationBytes AS BLOB(576K)"); // 16bit * 1 + 16bit * 8
stmt.execute("CREATE TYPE ATCode AS VARBINARY(1024)"); // was: 16bit * 1
stmt.execute("CREATE TYPE ATCreationBytes AS VARBINARY(4096)"); // was: 16bit * 1 + 16bit * 8
stmt.execute("CREATE TYPE ATMessage AS VARBINARY(32)");
stmt.execute("CREATE TYPE ATName AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD");
stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4
stmt.execute("CREATE TYPE ATState AS VARBINARY(1024)"); // was: 16bit * 8 + 16bit * 4 + 16bit * 4
stmt.execute("CREATE TYPE ATTags AS VARCHAR(80) COLLATE SQL_TEXT_UCC_NO_PAD");
stmt.execute("CREATE TYPE ATType AS VARCHAR(32) COLLATE SQL_TEXT_UCC_NO_PAD");
stmt.execute("CREATE TYPE ATStateHash as VARBINARY(32)");
@@ -142,7 +141,7 @@ public class HSQLDBDatabaseUpdates {
+ "transaction_count INTEGER NOT NULL, total_fees QortalAmount NOT NULL, transactions_signature Signature NOT NULL, "
+ "height INTEGER NOT NULL, minted_when EpochMillis NOT NULL, "
+ "minter QortalPublicKey NOT NULL, minter_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QortalAmount NOT NULL, "
+ "online_accounts BLOB(1M), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures BLOB(1M), "
+ "online_accounts VARBINARY(1204), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures VARBINARY(1M), "
+ "PRIMARY KEY (signature))");
// For finding blocks by height.
stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)");
@@ -618,6 +617,39 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TABLE PublicizeTransactions (signature Signature, nonce INT NOT NULL, " + TRANSACTION_KEYS + ")");
break;
case 20:
// Trade bot
stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state TINYINT NOT NULL, "
+ "creator_address QortalAddress NOT NULL, at_address QortalAddress, updated_when BIGINT NOT NULL, qort_amount QortalAmount NOT NULL, "
+ "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, "
+ "trade_native_address QortalAddress NOT NULL, secret VARBINARY(32) NOT NULL, hash_of_secret VARBINARY(32) NOT NULL, "
+ "trade_foreign_public_key VARBINARY(33) NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, "
+ "bitcoin_amount BIGINT NOT NULL, xprv58 VARCHAR(200), last_transaction_signature Signature, locktime_a BIGINT, "
+ "receiving_account_info VARBINARY(32) NOT NULL, PRIMARY KEY (trade_private_key))");
break;
case 21:
// AT functionality index
stmt.execute("CREATE INDEX IF NOT EXISTS ATCodeHashIndex ON ATs (code_hash, is_finished)");
break;
case 22:
// LOB downsizing
stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts VARBINARY(1024)");
stmt.execute("CHECKPOINT");
stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts_signatures VARBINARY(1048576)");
stmt.execute("CHECKPOINT");
stmt.execute("ALTER TABLE DeployATTransactions ALTER COLUMN creation_bytes VARBINARY(4096)");
stmt.execute("CHECKPOINT");
stmt.execute("ALTER TABLE ATs ALTER COLUMN code_bytes VARBINARY(1024)");
stmt.execute("CHECKPOINT");
stmt.execute("ALTER TABLE ATStates ALTER COLUMN state_data VARBINARY(1024)");
stmt.execute("CHECKPOINT");
break;
default:
// nothing to do
return false;

View File

@@ -32,6 +32,7 @@ import org.qortal.repository.ArbitraryRepository;
import org.qortal.repository.AssetRepository;
import org.qortal.repository.BlockRepository;
import org.qortal.repository.ChatRepository;
import org.qortal.repository.CrossChainRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository;
import org.qortal.repository.NameRepository;
@@ -115,6 +116,11 @@ public class HSQLDBRepository implements Repository {
return new HSQLDBChatRepository(this);
}
@Override
public CrossChainRepository getCrossChainRepository() {
return new HSQLDBCrossChainRepository(this);
}
@Override
public GroupRepository getGroupRepository() {
return new HSQLDBGroupRepository(this);

View File

@@ -19,6 +19,7 @@ import org.qortal.data.PaymentData;
import org.qortal.data.group.GroupApprovalData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.GroupApprovalTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.repository.DataException;
@@ -630,6 +631,43 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
@Override
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT signature from MessageTransactions "
+ "JOIN Transactions USING (signature) "
+ "JOIN BlockTransactions ON transaction_signature = signature "
+ "WHERE recipient = ?");
sql.append("ORDER BY Transactions.created_when");
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), recipient)) {
if (resultSet == null)
return messageTransactionsData;
do {
byte[] signature = resultSet.getBytes(1);
TransactionData transactionData = this.fromSignature(signature);
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
return null;
messageTransactionsData.add((MessageTransactionData) transactionData);
} while (resultSet.next());
return messageTransactionsData;
} catch (SQLException e) {
throw new DataException("Unable to fetch trade-bot messages from repository", e);
}
}
@Override
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
throws DataException {

View File

@@ -47,7 +47,7 @@ public class Settings {
// UI servers
private int uiPort = 12388;
private String[] uiLocalServers = new String[] {
"localhost", "172.24.1.1", "qor.tal"
"localhost", "127.0.0.1", "172.24.1.1", "qor.tal"
};
private String[] uiRemoteServers = new String[] {
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
@@ -92,6 +92,8 @@ public class Settings {
private int maxPeers = 32;
/** Maximum number of threads for network engine. */
private int maxNetworkThreadPoolSize = 20;
/** Maximum number of threads for network proof-of-work compute, used during handshaking. */
private int networkPoWComputePoolSize = 2;
// Which blockchains this node is running
private String blockchainConfig = null; // use default from resources
@@ -105,8 +107,8 @@ public class Settings {
// Auto-update sources
private String[] autoUpdateRepos = new String[] {
"https://github.com/QORT/qortal/raw/%s/qortal.update",
"https://raw.githubusercontent.com@151.101.16.133/QORT/qortal/%s/qortal.update"
"https://github.com/Qortal/qortal/raw/%s/qortal.update",
"https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update"
};
/** Array of NTP server hostnames. */
@@ -353,6 +355,10 @@ public class Settings {
return this.maxNetworkThreadPoolSize;
}
public int getNetworkPoWComputePoolSize() {
return this.networkPoWComputePoolSize;
}
public String getBlockchainConfig() {
return this.blockchainConfig;
}

View File

@@ -26,45 +26,47 @@ import com.google.common.base.Utf8;
public class DeployAtTransaction extends Transaction {
// Properties
private DeployAtTransactionData deployATTransactionData;
private DeployAtTransactionData deployAtTransactionData;
// Other useful constants
public static final int MAX_NAME_SIZE = 200;
public static final int MAX_DESCRIPTION_SIZE = 2000;
public static final int MAX_AT_TYPE_SIZE = 200;
public static final int MAX_TAGS_SIZE = 200;
public static final int MAX_CREATION_BYTES_SIZE = 100_000;
public static final int MAX_CREATION_BYTES_SIZE = 4096;
public static final int MAX_CODE_BYTES_LENGTH = 1024;
public static final int MAX_AT_STATE_LENGTH = 1024;
// Constructors
public DeployAtTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.deployATTransactionData = (DeployAtTransactionData) this.transactionData;
this.deployAtTransactionData = (DeployAtTransactionData) this.transactionData;
}
// More information
@Override
public List<String> getRecipientAddresses() throws DataException {
return Collections.singletonList(this.deployATTransactionData.getAtAddress());
return Collections.singletonList(this.deployAtTransactionData.getAtAddress());
}
/** Returns AT version from the header bytes */
private short getVersion() {
byte[] creationBytes = deployATTransactionData.getCreationBytes();
byte[] creationBytes = deployAtTransactionData.getCreationBytes();
return (short) ((creationBytes[0] << 8) | (creationBytes[1] & 0xff)); // Big-endian
}
/** Make sure deployATTransactionData has an ATAddress */
private void ensureATAddress() throws DataException {
if (this.deployATTransactionData.getAtAddress() != null)
public static void ensureATAddress(DeployAtTransactionData deployAtTransactionData) throws DataException {
if (deployAtTransactionData.getAtAddress() != null)
return;
// Use transaction transformer
try {
String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(this.deployATTransactionData));
this.deployATTransactionData.setAtAddress(atAddress);
String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(deployAtTransactionData));
deployAtTransactionData.setAtAddress(atAddress);
} catch (TransformationException e) {
throw new DataException("Unable to generate AT address");
}
@@ -73,9 +75,9 @@ public class DeployAtTransaction extends Transaction {
// Navigation
public Account getATAccount() throws DataException {
ensureATAddress();
ensureATAddress(this.deployAtTransactionData);
return new Account(this.repository, this.deployATTransactionData.getAtAddress());
return new Account(this.repository, this.deployAtTransactionData.getAtAddress());
}
// Processing
@@ -83,30 +85,30 @@ public class DeployAtTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
// Check name size bounds
int nameLength = Utf8.encodedLength(this.deployATTransactionData.getName());
int nameLength = Utf8.encodedLength(this.deployAtTransactionData.getName());
if (nameLength < 1 || nameLength > MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check description size bounds
int descriptionlength = Utf8.encodedLength(this.deployATTransactionData.getDescription());
int descriptionlength = Utf8.encodedLength(this.deployAtTransactionData.getDescription());
if (descriptionlength < 1 || descriptionlength > MAX_DESCRIPTION_SIZE)
return ValidationResult.INVALID_DESCRIPTION_LENGTH;
// Check AT-type size bounds
int atTypeLength = Utf8.encodedLength(this.deployATTransactionData.getAtType());
int atTypeLength = Utf8.encodedLength(this.deployAtTransactionData.getAtType());
if (atTypeLength < 1 || atTypeLength > MAX_AT_TYPE_SIZE)
return ValidationResult.INVALID_AT_TYPE_LENGTH;
// Check tags size bounds
int tagsLength = Utf8.encodedLength(this.deployATTransactionData.getTags());
int tagsLength = Utf8.encodedLength(this.deployAtTransactionData.getTags());
if (tagsLength < 1 || tagsLength > MAX_TAGS_SIZE)
return ValidationResult.INVALID_TAGS_LENGTH;
// Check amount is positive
if (this.deployATTransactionData.getAmount() <= 0)
if (this.deployAtTransactionData.getAmount() <= 0)
return ValidationResult.NEGATIVE_AMOUNT;
long assetId = this.deployATTransactionData.getAssetId();
long assetId = this.deployAtTransactionData.getAssetId();
AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
// Check asset even exists
if (assetData == null)
@@ -117,7 +119,7 @@ public class DeployAtTransaction extends Transaction {
return ValidationResult.ASSET_NOT_SPENDABLE;
// Check asset amount is integer if asset is not divisible
if (!assetData.isDivisible() && this.deployATTransactionData.getAmount() % Amounts.MULTIPLIER != 0)
if (!assetData.isDivisible() && this.deployAtTransactionData.getAmount() % Amounts.MULTIPLIER != 0)
return ValidationResult.INVALID_AMOUNT;
Account creator = this.getCreator();
@@ -125,15 +127,15 @@ public class DeployAtTransaction extends Transaction {
// Check creator has enough funds
if (assetId == Asset.QORT) {
// Simple case: amount and fee both in QORT
long minimumBalance = this.deployATTransactionData.getFee() + this.deployATTransactionData.getAmount();
long minimumBalance = this.deployAtTransactionData.getFee() + this.deployAtTransactionData.getAmount();
if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
return ValidationResult.NO_BALANCE;
} else {
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee())
if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
return ValidationResult.NO_BALANCE;
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount())
if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
return ValidationResult.NO_BALANCE;
}
@@ -142,12 +144,12 @@ public class DeployAtTransaction extends Transaction {
return ValidationResult.INVALID_CREATION_BYTES;
// Check creation bytes are valid (for v2+)
this.ensureATAddress();
ensureATAddress(this.deployAtTransactionData);
// Just enough AT data to allow API to query initial balances, etc.
String atAddress = this.deployATTransactionData.getAtAddress();
byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey();
long creation = this.deployATTransactionData.getTimestamp();
String atAddress = this.deployAtTransactionData.getAtAddress();
byte[] creatorPublicKey = this.deployAtTransactionData.getCreatorPublicKey();
long creation = this.deployAtTransactionData.getTimestamp();
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
@@ -157,7 +159,15 @@ public class DeployAtTransaction extends Transaction {
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
try {
new MachineState(api, loggerFactory, this.deployATTransactionData.getCreationBytes());
MachineState state = new MachineState(api, loggerFactory, this.deployAtTransactionData.getCreationBytes());
byte[] codeBytes = state.getCodeBytes();
if (codeBytes == null || codeBytes.length > MAX_CODE_BYTES_LENGTH)
return ValidationResult.INVALID_CREATION_BYTES;
byte[] atStateBytes = state.toBytes();
if (atStateBytes == null || atStateBytes.length > MAX_AT_STATE_LENGTH)
return ValidationResult.INVALID_CREATION_BYTES;
} catch (IllegalArgumentException e) {
// Not valid
return ValidationResult.INVALID_CREATION_BYTES;
@@ -169,25 +179,25 @@ public class DeployAtTransaction extends Transaction {
@Override
public ValidationResult isProcessable() throws DataException {
Account creator = getCreator();
long assetId = this.deployATTransactionData.getAssetId();
long assetId = this.deployAtTransactionData.getAssetId();
// Check creator has enough funds
if (assetId == Asset.QORT) {
// Simple case: amount and fee both in QORT
long minimumBalance = this.deployATTransactionData.getFee() + this.deployATTransactionData.getAmount();
long minimumBalance = this.deployAtTransactionData.getFee() + this.deployAtTransactionData.getAmount();
if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
return ValidationResult.NO_BALANCE;
} else {
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee())
if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
return ValidationResult.NO_BALANCE;
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount())
if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
return ValidationResult.NO_BALANCE;
}
// Check AT doesn't already exist
if (this.repository.getATRepository().exists(this.deployATTransactionData.getAtAddress()))
if (this.repository.getATRepository().exists(this.deployAtTransactionData.getAtAddress()))
return ValidationResult.AT_ALREADY_EXISTS;
return ValidationResult.OK;
@@ -195,40 +205,40 @@ public class DeployAtTransaction extends Transaction {
@Override
public void process() throws DataException {
this.ensureATAddress();
ensureATAddress(this.deployAtTransactionData);
// Deploy AT, saving into repository
AT at = new AT(this.repository, this.deployATTransactionData);
AT at = new AT(this.repository, this.deployAtTransactionData);
at.deploy();
long assetId = this.deployATTransactionData.getAssetId();
long assetId = this.deployAtTransactionData.getAssetId();
// Update creator's balance regarding initial payment to AT
Account creator = getCreator();
creator.modifyAssetBalance(assetId, - this.deployATTransactionData.getAmount());
creator.modifyAssetBalance(assetId, - this.deployAtTransactionData.getAmount());
// Update AT's reference, which also creates AT account
Account atAccount = this.getATAccount();
atAccount.setLastReference(this.deployATTransactionData.getSignature());
atAccount.setLastReference(this.deployAtTransactionData.getSignature());
// Update AT's balance
atAccount.setConfirmedBalance(assetId, this.deployATTransactionData.getAmount());
atAccount.setConfirmedBalance(assetId, this.deployAtTransactionData.getAmount());
}
@Override
public void orphan() throws DataException {
// Delete AT from repository
AT at = new AT(this.repository, this.deployATTransactionData);
AT at = new AT(this.repository, this.deployAtTransactionData);
at.undeploy();
long assetId = this.deployATTransactionData.getAssetId();
long assetId = this.deployAtTransactionData.getAssetId();
// Update creator's balance regarding initial payment to AT
Account creator = getCreator();
creator.modifyAssetBalance(assetId, this.deployATTransactionData.getAmount());
creator.modifyAssetBalance(assetId, this.deployAtTransactionData.getAmount());
// Delete AT's account (and hence its balance)
this.repository.getAccountRepository().delete(this.deployATTransactionData.getAtAddress());
this.repository.getAccountRepository().delete(this.deployAtTransactionData.getAtAddress());
}
}

View File

@@ -136,7 +136,7 @@ public class RewardShareTransaction extends Transaction {
// Deleting a non-existent reward-share makes no sense
if (isCancellingSharePercent)
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
return ValidationResult.REWARD_SHARE_UNKNOWN;
// Check the minting account hasn't reach maximum number of reward-shares
int rewardShareCount = this.repository.getAccountRepository().countRewardShares(creator.getPublicKey());

View File

@@ -225,6 +225,7 @@ public abstract class Transaction {
AT_IS_FINISHED(71),
NO_FLAG_PERMISSION(72),
NOT_MINTING_ACCOUNT(73),
REWARD_SHARE_UNKNOWN(76),
INVALID_REWARD_SHARE_PERCENT(77),
PUBLIC_KEY_UNKNOWN(78),
INVALID_PUBLIC_KEY(79),

View File

@@ -26,9 +26,26 @@ public class BitTwiddling {
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) };
}
/** Convert int to big-endian byte array */
public static byte[] toBEByteArray(int value) {
return new byte[] { (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) };
}
/** Convert long to big-endian byte array */
public static byte[] toBEByteArray(long value) {
return new byte[] { (byte) (value >> 56), (byte) (value >> 48), (byte) (value >> 40), (byte) (value >> 32),
(byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) };
}
/** Convert little-endian bytes to int */
public static int fromLEBytes(byte[] bytes, int offset) {
public static int intFromLEBytes(byte[] bytes, int offset) {
return (bytes[offset] & 0xff) | (bytes[offset + 1] & 0xff) << 8 | (bytes[offset + 2] & 0xff) << 16 | (bytes[offset + 3] & 0xff) << 24;
}
/** Convert big-endian bytes to long */
public static long longFromBEBytes(byte[] bytes, int start) {
return (bytes[start] & 0xffL) << 56 | (bytes[start + 1] & 0xffL) << 48 | (bytes[start + 2] & 0xffL) << 40 | (bytes[start + 3] & 0xffL) << 32
| (bytes[start + 4] & 0xffL) << 24 | (bytes[start + 5] & 0xffL) << 16 | (bytes[start + 6] & 0xffL) << 8 | (bytes[start + 7] & 0xffL);
}
}

View File

@@ -0,0 +1,31 @@
package org.qortal.utils;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class DaemonThreadFactory implements ThreadFactory {
private final String name;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public DaemonThreadFactory(String name) {
this.name = name;
}
public DaemonThreadFactory() {
this(null);
}
@Override
public Thread newThread(Runnable runnable) {
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setDaemon(true);
if (this.name != null)
thread.setName(this.name + "-" + this.threadNumber.getAndIncrement());
return thread;
}
}

View File

@@ -0,0 +1,24 @@
package org.qortal.utils;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class NamedThreadFactory implements ThreadFactory {
private final String name;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public NamedThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable runnable) {
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setName(this.name + "-" + this.threadNumber.getAndIncrement());
return thread;
}
}

View File

@@ -0,0 +1,220 @@
package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.Random;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.at.QortalFunctionCode;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.AccountUtils;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.BitTwiddling;
public class GetMessageLengthTests extends Common {
private static final Random RANDOM = new Random();
@Before
public void before() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testGetMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
byte[] creationBytes = buildMessageLengthAT();
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Send messages with known length
checkMessageLength(repository, deployer, atAddress, 1);
checkMessageLength(repository, deployer, atAddress, 10);
checkMessageLength(repository, deployer, atAddress, 32);
checkMessageLength(repository, deployer, atAddress, 99);
// Finally, send a payment instead and check returned length is -1
AccountUtils.pay(repository, deployer, atAddress, 123L);
// Mint another block so AT can process payment
BlockUtils.mintBlock(repository);
// Check AT result
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
byte[] dataBytes = MachineState.extractDataBytes(stateData);
long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0);
assertEquals(-1L, extractedLength);
}
}
private void checkMessageLength(Repository repository, PrivateKeyAccount sender, String atAddress, int messageLength) throws DataException {
byte[] testMessage = new byte[messageLength];
RANDOM.nextBytes(testMessage);
sendMessage(repository, sender, testMessage, atAddress);
// Mint another block so AT can process message
BlockUtils.mintBlock(repository);
// Check AT result
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
byte[] dataBytes = MachineState.extractDataBytes(stateData);
long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0);
assertEquals(messageLength, extractedLength);
}
private byte[] buildMessageLengthAT() {
// Labels for data segment addresses
int addrCounter = 0;
// Make result first for easier extraction
final int addrResult = addrCounter++;
final int addrLastTxTimestamp = addrCounter++;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// Code labels
Integer labelCheckTx = null;
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
/* Initialization */
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message to AT */
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, OpCode.calcOffset(codeByteBuffer, labelCheckTx)));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */
labelCheckTx = codeByteBuffer.position();
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
// Save message length
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrResult));
// Stop and wait for next block (and hence more transactions)
codeByteBuffer.put(OpCode.STP_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
}

View File

@@ -0,0 +1,266 @@
package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.utils.BitTwiddling;
public class GetNextTransactionTests extends Common {
@Before
public void before() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testGetNextTransaction() throws DataException {
byte[] data = new byte[] { 0x44 };
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
byte[] creationBytes = buildGetNextTransactionAT();
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
byte[] rawNextTimestamp = new byte[32];
Transaction transaction;
// Confirm initial value is zero
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
// Send message to someone other than AT
sendMessage(repository, deployer, data, deployer.getAddress());
BlockUtils.mintBlock(repository);
// Confirm AT does not find message
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
// Send message to AT
transaction = sendMessage(repository, deployer, data, atAddress);
BlockUtils.mintBlock(repository);
// Confirm AT finds message
BlockUtils.mintBlock(repository);
assertTimestamp(repository, atAddress, transaction);
// Mint a few blocks, then send non-AT message, followed by AT message
for (int i = 0; i < 5; ++i)
BlockUtils.mintBlock(repository);
sendMessage(repository, deployer, data, deployer.getAddress());
transaction = sendMessage(repository, deployer, data, atAddress);
BlockUtils.mintBlock(repository);
// Confirm AT finds message
BlockUtils.mintBlock(repository);
assertTimestamp(repository, atAddress, transaction);
}
}
private byte[] buildGetNextTransactionAT() {
// Labels for data segment addresses
int addrCounter = 0;
// Beginning of data segment for easy extraction
final int addrNextTx = addrCounter;
addrCounter += 4;
final int addrNextTxIndex = addrCounter++;
final int addrLastTxTimestamp = addrCounter++;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// skip addrNextTx
dataByteBuffer.position(dataByteBuffer.position() + 4 * MachineState.VALUE_SIZE);
// Store pointer to addrNextTx at addrNextTxIndex
dataByteBuffer.putLong(addrNextTx);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
/* Initialization */
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message to AT */
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
// Copy A to data segment, starting at addrNextTx (as pointed to by addrNextTxIndex)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_A_IND, addrNextTxIndex));
// Stop if timestamp part of A is zero
codeByteBuffer.put(OpCode.STZ_DAT.compile(addrNextTx));
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private void extractNextTxTimestamp(Repository repository, String atAddress, byte[] rawNextTimestamp) throws DataException {
// Check AT result
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
byte[] dataBytes = MachineState.extractDataBytes(stateData);
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndImportValid(repository, messageTransactionData, sender);
return messageTransaction;
}
private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException {
int height = transaction.getHeight();
byte[] transactionSignature = transaction.getTransactionData().getSignature();
BlockData blockData = repository.getBlockRepository().fromHeight(height);
assertNotNull(blockData);
Block block = new Block(repository, blockData);
List<Transaction> blockTransactions = block.getTransactions();
int sequence;
for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence)
if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature))
break;
assertNotSame(-1, sequence);
byte[] rawNextTimestamp = new byte[32];
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
Timestamp expectedTimestamp = new Timestamp(height, sequence);
Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawNextTimestamp, 0));
assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d",
height, sequence,
actualTimestamp.blockHeight, actualTimestamp.transactionSequence
),
expectedTimestamp.longValue(),
actualTimestamp.longValue());
byte[] expectedPartialSignature = new byte[24];
System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length);
byte[] actualPartialSignature = new byte[24];
System.arraycopy(rawNextTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length);
assertArrayEquals(expectedPartialSignature, actualPartialSignature);
}
}

View File

@@ -0,0 +1,219 @@
package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.at.QortalFunctionCode;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
public class GetPartialMessageTests extends Common {
@Before
public void before() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testGetPartialMessage() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
byte[] messageData = "The quick brown fox jumped over the lazy dog.".getBytes();
int[] offsets = new int[] { 0, 7, 32, 44, messageData.length };
byte[] creationBytes = buildGetPartialMessageAT(offsets);
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
sendMessage(repository, deployer, messageData, atAddress);
for (int offset : offsets) {
// Mint another block so AT can process message
BlockUtils.mintBlock(repository);
byte[] expectedData = new byte[32];
int byteCount = Math.min(32, messageData.length - offset);
System.arraycopy(messageData, offset, expectedData, 0, byteCount);
// Check AT result
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
byte[] dataBytes = MachineState.extractDataBytes(stateData);
byte[] actualData = new byte[32];
System.arraycopy(dataBytes, MachineState.VALUE_SIZE, actualData, 0, 32);
assertArrayEquals(expectedData, actualData);
}
}
}
private byte[] buildGetPartialMessageAT(int... offsets) {
// Labels for data segment addresses
int addrCounter = 0;
final int addrCopyOfBIndex = addrCounter++;
// 2nd position for easy extraction
final int addrCopyOfB = addrCounter;
addrCounter += 4;
final int addrResult = addrCounter++;
final int addrLastTxTimestamp = addrCounter++;
final int addrOffset = addrCounter++;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
dataByteBuffer.putLong(addrCopyOfB);
// Code labels
Integer labelCheckTx = null;
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
/* Initialization */
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message to AT */
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, OpCode.calcOffset(codeByteBuffer, labelCheckTx)));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */
labelCheckTx = codeByteBuffer.position();
// Generate code per offset
for (int i = 0; i < offsets.length; ++i) {
if (i > 0)
// Wait for next block
codeByteBuffer.put(OpCode.SLP_IMD.compile());
// Set offset
codeByteBuffer.put(OpCode.SET_VAL.compile(addrOffset, offsets[i]));
// Extract partial message
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrOffset));
// Copy B to data segment
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCopyOfBIndex));
}
// We're done
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
}

View File

@@ -1,7 +1,6 @@
package org.qortal.test.btcacct;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -10,14 +9,15 @@ import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import org.bitcoinj.core.Base58;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
@@ -43,14 +43,18 @@ import com.google.common.primitives.Bytes;
public class AtTests extends Common {
public static final byte[] secret = "This string is exactly 32 bytes!".getBytes();
public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final int refundTimeout = 10; // blocks
public static final long initialPayout = 100000L;
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes();
public static final byte[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58
public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long bitcoinAmount = 864200L;
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@@ -58,9 +62,9 @@ public class AtTests extends Common {
@Test
public void testCompile() {
Account deployer = Common.getTestAccount(null, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
byte[] creationBytes = BTCACCT.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@@ -68,12 +72,14 @@ public class AtTests extends Common {
public void testDeploy() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
@@ -85,10 +91,10 @@ public class AtTests extends Common {
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance;
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Recipient's post-deployment balance incorrect", expectedBalance, actualBalance);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
@@ -103,10 +109,10 @@ public class AtTests extends Common {
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance;
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Recipient's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
}
@@ -115,26 +121,39 @@ public class AtTests extends Common {
public void testOfferCancel() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = BTCACCT.buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// Refund should happen 1st block after receiving recipient address
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
@@ -143,11 +162,10 @@ public class AtTests extends Common {
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
describeAt(repository, atAddress);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
@@ -157,71 +175,144 @@ public class AtTests extends Common {
@SuppressWarnings("unused")
@Test
public void testInitialPayment() throws DataException {
public void testOfferCancelInvalidLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Send recipient's address to AT
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Initial payment should happen 1st block after receiving recipient address
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance + initialPayout;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
expectedBalance = recipientsInitialBalance;
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
assertEquals("Recipient's pre-initial-payout balance incorrect", expectedBalance, actualBalance);
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
}
}
// TEST SENDING RECIPIENT ADDRESS BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
@SuppressWarnings("unused")
@Test
public void testTradingInfoProcessing() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's Bitcoin PKH was extracted correctly
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerBitcoinPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
@SuppressWarnings("unused")
@Test
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Send recipient's address to AT BUT NOT FROM AT CREATOR
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, bystander, recipientAddressBytes, atAddress);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// Initial payment should NOT happen
BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(BTCACCT.Mode.OFFERING, tradeData.mode);
}
}
@@ -230,34 +321,48 @@ public class AtTests extends Common {
public void testAutomaticTradeRefund() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Send recipient's address to AT
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Initial payment should happen 1st block after receiving recipient address
BlockUtils.mintBlock(repository);
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long messageFee = messageTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee - messageFee;
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
BlockUtils.orphanLastBlock(repository);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
@@ -267,46 +372,63 @@ public class AtTests extends Common {
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSender() throws DataException {
public void testCorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Send recipient's address to AT
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Initial payment should happen 1st block after receiving recipient address
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT
messageTransaction = sendMessage(repository, recipient, secret, atAddress);
// Send correct secrets to AT, from correct account
messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
@@ -317,99 +439,206 @@ public class AtTests extends Common {
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
public void testCorrectSecretsIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Send recipient's address to AT
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Initial payment should happen 1st block after receiving recipient address
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageTransaction = sendMessage(repository, bystander, secret, atAddress);
// Send correct secrets to AT, but from wrong account
messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance + initialPayout;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
public void testIncorrectSecretsCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Send recipient's address to AT
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Initial payment should happen 1st block after receiving recipient address
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
byte[] wrongSecret = Crypto.digest(secret);
messageTransaction = sendMessage(repository, recipient, wrongSecret, atAddress);
// Send incorrect secrets to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
describeAt(repository, atAddress);
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Send incorrect secrets to AT, from correct account
messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() * 2;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretsCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secrets to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA, secretB);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
@@ -433,8 +662,12 @@ public class AtTests extends Common {
}
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = BTCACCT.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
@@ -493,6 +726,7 @@ public class AtTests extends Common {
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout * 3 / 4 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
@@ -500,7 +734,7 @@ public class AtTests extends Common {
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - initialPayout;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
@@ -516,40 +750,43 @@ public class AtTests extends Common {
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tHASH160 of secret: %s,\n"
+ "\tinitial payout: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tHASH160 of secret-B: %s,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected bitcoin: %s BTC,\n"
+ "\ttrade timeout: %d minutes (from trade start),\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode.name(),
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40),
Amounts.prettyAmount(tradeData.initialPayout),
Amounts.prettyAmount(tradeData.redeemPayout),
atData.getIsFinished(),
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedBitcoin),
tradeData.tradeRefundTimeout,
currentBlockHeight));
// Are we in 'offer' or 'trade' stage?
if (tradeData.tradeRefundHeight == null) {
// Offer
System.out.println(String.format("\tstatus: 'offer mode'"));
} else {
// Trade
System.out.println(String.format("\tstatus: 'trade mode',\n"
+ "\ttrade timeout: block %d,\n"
+ "\tBitcoin P2SH nLockTime: %d (%s),\n"
+ "\ttrade recipient: %s",
if (tradeData.mode != BTCACCT.Mode.OFFERING && tradeData.mode != BTCACCT.Mode.CANCELLED) {
System.out.println(String.format("\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n"
+ "\tBitcoin P2SH-B nLockTime: %d (%s),\n"
+ "\ttrade partner: %s",
tradeData.tradeRefundHeight,
tradeData.lockTime, epochMilliFormatter.apply(tradeData.lockTime * 1000L),
tradeData.qortalRecipient));
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.lockTimeB, epochMilliFormatter.apply(tradeData.lockTimeB * 1000L),
tradeData.qortalPartnerAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
}

View File

@@ -5,12 +5,13 @@ import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
@@ -55,11 +56,52 @@ public class BtcTests extends Common {
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
byte[] expectedSecret = AtTests.secret;
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, rawTransactions);
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testBuildSpend() {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = btc.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
// Check spent key caching doesn't affect outcome
transaction = btc.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
}
@Test
public void testGetWalletBalance() {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = btc.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(BTC.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = btc.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(BTC.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
}

View File

@@ -13,7 +13,7 @@ import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -98,12 +98,12 @@ public class BuildP2SH {
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@@ -115,7 +115,7 @@ public class BuildP2SH {
// Fund P2SH
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)",
p2shAddress.toString(), BTC.FORMAT.format(bitcoinAmount), BTC.FORMAT.format(bitcoinFee)));
p2shAddress.toString(), BTC.format(bitcoinAmount), BTC.format(bitcoinFee)));
System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT");
} catch (DataException e) {

View File

@@ -15,7 +15,7 @@ import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -106,14 +106,14 @@ public class CheckP2SH {
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress));
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
System.out.println(String.format("P2SH address: %s", p2shAddress));
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@@ -135,12 +135,12 @@ public class CheckP2SH {
System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
// Check P2SH is funded
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
}
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.FORMAT.format(p2shBalance)));
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
@@ -152,7 +152,7 @@ public class CheckP2SH {
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));

View File

@@ -34,20 +34,20 @@ public class DeployAT {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <HASH160-of-secret> <initial QORT payout> <AT funding amount> <AT trade timeout>"));
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <your Bitcoin PKH> <HASH160-of-secret> <AT funding amount> <trade-timeout>"));
System.err.println(String.format("example: DeployAT "
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
+ "\t80.4020 \\\n"
+ "\t0.00864200 \\\n"
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t0.0001 \\\n"
+ "\t123.456 \\\n"
+ "\t10"));
+ "\t10080"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 8)
if (args.length != 7)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
@@ -56,8 +56,8 @@ public class DeployAT {
byte[] refundPrivateKey = null;
long redeemAmount = 0;
long expectedBitcoin = 0;
byte[] bitcoinPublicKeyHash = null;
byte[] secretHash = null;
long initialPayout = 0;
long fundingAmount = 0;
int tradeTimeout = 0;
@@ -75,21 +75,21 @@ public class DeployAT {
if (expectedBitcoin <= 0)
usage("Expected BTC amount must be positive");
bitcoinPublicKeyHash = HashCode.fromString(args[argIndex++]).asBytes();
if (bitcoinPublicKeyHash.length != 20)
usage("Bitcoin PKH must be 20 bytes");
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
if (secretHash.length != 20)
usage("Hash of secret must be 20 bytes");
initialPayout = Long.parseLong(args[argIndex++]);
if (initialPayout < 0)
usage("Initial QORT payout must be positive");
fundingAmount = Long.parseLong(args[argIndex++]);
if (fundingAmount <= redeemAmount)
usage("AT funding amount must be greater than QORT redeem amount");
tradeTimeout = Integer.parseInt(args[argIndex++]);
if (tradeTimeout < 10 || tradeTimeout > 50000)
usage("AT trade timeout should be between 10 and 50,000 minutes");
if (tradeTimeout < 60 || tradeTimeout > 50000)
usage("Trade timeout (minutes) must be between 60 and 50000");
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
@@ -114,7 +114,7 @@ public class DeployAT {
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
// Deploy AT
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), secretHash, tradeTimeout, initialPayout, redeemAmount, expectedBitcoin);
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
long txTimestamp = System.currentTimeMillis();

View File

@@ -12,8 +12,8 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.junit.Test;
import org.qortal.crosschain.ElectrumX;
import org.qortal.crosschain.ElectrumX.UnspentOutput;
import org.qortal.utils.BitTwiddling;
import org.qortal.utils.Pair;
import com.google.common.hash.HashCode;
@@ -61,7 +61,7 @@ public class ElectrumXTests {
// Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset
int offset = 4 + 32 + 32;
int timestamp = BitTwiddling.fromLEBytes(blockHeader, offset);
int timestamp = BitTwiddling.intFromLEBytes(blockHeader, offset);
System.out.println(String.format("Block %d timestamp: %d", height + i, timestamp));
}
}
@@ -100,13 +100,13 @@ public class ElectrumXTests {
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<Pair<byte[], Integer>> unspentOutputs = electrumX.getUnspentOutputs(script);
List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script);
assertNotNull(unspentOutputs);
assertFalse(unspentOutputs.isEmpty());
for (Pair<byte[], Integer> unspentOutput : unspentOutputs)
System.out.println(String.format("TestNet address %s has unspent output at tx %s, output index %d", address, HashCode.fromBytes(unspentOutput.getA()).toString(), unspentOutput.getB()));
for (UnspentOutput unspentOutput : unspentOutputs)
System.out.println(String.format("TestNet address %s has unspent output at tx %s, output index %d", address, HashCode.fromBytes(unspentOutput.hash), unspentOutput.index));
}
@Test

View File

@@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -107,7 +107,7 @@ public class Redeem {
System.out.println("Confirm the following is correct based on the info you've given:");
System.out.println(String.format("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey)));
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
// New/derived info
@@ -121,7 +121,7 @@ public class Redeem {
System.out.println(String.format("P2SH address: %s", p2shAddress));
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@@ -147,12 +147,12 @@ public class Redeem {
}
// Check P2SH is funded
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
}
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
@@ -164,7 +164,7 @@ public class Redeem {
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
@@ -179,10 +179,10 @@ public class Redeem {
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
Coin redeemAmount = p2shBalance.subtract(bitcoinFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(redeemAmount), BTC.FORMAT.format(bitcoinFee)));
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee)));
Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret);
Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret, redeemAddress.getHash());
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();

View File

@@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -110,7 +110,7 @@ public class Refund {
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
System.out.println(String.format("P2SH address: %s", p2shAddress));
System.out.println(String.format("Refund miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
System.out.println(String.format("Refund miner's fee: %s", BTC.format(bitcoinFee)));
// New/derived info
@@ -120,7 +120,7 @@ public class Refund {
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash())));
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@@ -151,12 +151,12 @@ public class Refund {
}
// Check P2SH is funded
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
}
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
@@ -168,7 +168,7 @@ public class Refund {
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't refund spent/unfunded P2SH"));
@@ -183,10 +183,10 @@ public class Refund {
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
Coin refundAmount = p2shBalance.subtract(bitcoinFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(refundAmount), BTC.FORMAT.format(bitcoinFee)));
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();

View File

@@ -20,15 +20,19 @@ public class AccountUtils {
public static final int txGroupId = Group.NO_GROUP;
public static final long fee = 1L * Amounts.MULTIPLIER;
public static void pay(Repository repository, String sender, String recipient, long amount) throws DataException {
PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, sender);
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient);
public static void pay(Repository repository, String testSenderName, String testRecipientName, long amount) throws DataException {
PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, testSenderName);
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, testRecipientName);
pay(repository, sendingAccount, recipientAccount.getAddress(), amount);
}
public static void pay(Repository repository, PrivateKeyAccount sendingAccount, String recipientAddress, long amount) throws DataException {
byte[] reference = sendingAccount.getLastReference();
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, sendingAccount.getPublicKey(), fee, null);
TransactionData transactionData = new PaymentTransactionData(baseTransactionData, recipientAccount.getAddress(), amount);
TransactionData transactionData = new PaymentTransactionData(baseTransactionData, recipientAddress, amount);
TransactionUtils.signAndMint(repository, transactionData, sendingAccount);
}

13
tools/peer-heights Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Requires: 'qort' script in PATH, and 'jq' utility installed
set -e
# Any extra args passed to us are also passed to 'qort', just prior to '-p peers'
qort $@ -p peers | \
jq -r 'def lpad($len):
tostring | ($len - length) as $l | (" " * $l)[:$l] + .;
.[] |
select(has("lastHeight")) |
"\(.address | lpad(22)) (\(.version)), height \(.lastHeight), sig: \(.lastBlockSignature[0:8]), ts \(.lastBlockTimestamp / 1e3 | strftime("%Y-%m-%d %H:%M:%S"))"'

87
tools/qort Executable file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# default output post-processor
postproc=cat
# Qortal defaults
port=12391
example_host=node10.qortal.org
# called-as name
name="${0##*/}"
while [ -n "$*" ]; do
case $1 in
-p)
shift
postproc="json_pp -f json -t json -json_opt utf8,pretty"
;;
[Dd][Ee][Ll][Ee][Tt][Ee])
shift
method="-X DELETE"
;;
-l)
shift
src="--interface localhost"
;;
-t)
shift
testnet=true
;;
-j)
shift
content_type="Content-Type: application/json"
;;
*)
break
;;
esac
done
if [ "${name}" = "qort" ]; then
port=${testnet:+62391}
port=${port:-12391}
example_host=node10.qortal.org
fi
if [ -z "$*" ]; then
echo "usage: $name [-l] [-p] [-t] [DELETE] <url> [<post-data>]"
echo "-l: use localhost as source address"
echo "-p: pretty-print JSON output"
echo "-t: use testnet port"
echo "example (using localhost:${port}): $name -p blocks/last"
echo "example: $name -p http://${example_host}:${port}/blocks/last"
echo "example: BASE_URL=http://${example_host}:${port} $name -p blocks/last"
echo "example: BASE_URL=${example_host} $name -p blocks/last"
echo "example: $name -l DELETE peers/known"
exit
fi
url=$1
shift
if [ "${url:0:4}" != "http" ]; then
base_url=${BASE_URL-localhost:${port}}
if [ "${base_url:0:4}" != "http" ]; then
base_url="http://${base_url}"
fi
if [ -n "${base_url/#*:[0-9[0-9]*}" ]; then
base_url="${base_url%%/}:${port}"
fi
url="${base_url%%/}/${url#/}"
fi
if [ "$#" != 0 ]; then
data="--data"
fi
curl --silent --insecure --connect-timeout 5 ${content_type:+--header} "${content_type}" ${method} ${src} --url ${url} ${data} "$@" | ${postproc}
echo

353
tools/tx.pl Executable file
View File

@@ -0,0 +1,353 @@
#!/usr/bin/env perl
use JSON;
use warnings;
use strict;
use Getopt::Std;
use File::Basename;
our %opt;
getopts('dpst', \%opt);
my $proc = basename($0);
if (@ARGV < 1) {
print STDERR "usage: $proc [-d] [-p] [-s] [-t] <tx-type> <privkey> <values> [<key-value pairs>]\n";
print STDERR "-d: debug, -p: process (broadcast) transaction, -s: sign, -t: testnet\n";
print STDERR "example: $proc PAYMENT P22kW91AJfDNBj32nVii292hhfo5AgvUYPz5W12ExsjE QxxQZiK7LZBjmpGjRz1FAZSx9MJDCoaHqz 0.1\n";
print STDERR "example: $proc JOIN_GROUP X92h3hf9k20kBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n";
print STDERR "example: BASE_URL=node10.qortal.org $proc JOIN_GROUP CB2DW91AJfd47432nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n";
print STDERR "example: $proc -p sign C4ifh827ffDNBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 111jivxUwerRw...Fjtu\n";
print STDERR "for help: $proc all\n";
print STDERR "for help: $proc REGISTER_NAME\n";
exit 2;
}
our $BASE_URL = $ENV{BASE_URL} || $opt{t} ? 'http://localhost:62391' : 'http://localhost:12391';
our $DEFAULT_FEE = 0.001;
our %TRANSACTION_TYPES = (
payment => {
url => 'payments/pay',
required => [qw(recipient amount)],
key_name => 'senderPublicKey',
},
# groups
set_group => {
url => 'groups/setdefault',
required => [qw(defaultGroupId)],
key_name => 'creatorPublicKey',
},
create_group => {
url => 'groups/create',
required => [qw(groupName description isOpen approvalThreshold)],
key_name => 'creatorPublicKey',
},
update_group => {
url => 'groups/update',
required => [qw(groupId newOwner newDescription newIsOpen newApprovalThreshold)],
key_name => 'ownerPublicKey',
},
join_group => {
url => 'groups/join',
required => [qw(groupId)],
key_name => 'joinerPublicKey',
},
leave_group => {
url => 'groups/leave',
required => [qw(groupId)],
key_name => 'leaverPublicKey',
},
group_invite => {
url => 'groups/invite',
required => [qw(groupId invitee)],
key_name => 'adminPublicKey',
},
group_kick => {
url => 'groups/kick',
required => [qw(groupId member reason)],
key_name => 'adminPublicKey',
},
add_group_admin => {
url => 'groups/addadmin',
required => [qw(groupId member)],
key_name => 'ownerPublicKey',
},
group_approval => {
url => 'groups/approval',
required => [qw(pendingSignature approval)],
key_name => 'adminPublicKey',
},
# assets
issue_asset => {
url => 'assets/issue',
required => [qw(assetName description quantity isDivisible)],
key_name => 'issuerPublicKey',
},
update_asset => {
url => 'assets/update',
required => [qw(assetId newOwner)],
key_name => 'ownerPublicKey',
},
transfer_asset => {
url => 'assets/transfer',
required => [qw(recipient amount assetId)],
key_name => 'senderPublicKey',
},
create_order => {
url => 'assets/order',
required => [qw(haveAssetId wantAssetId amount price)],
key_name => 'creatorPublicKey',
},
# names
register_name => {
url => 'names/register',
required => [qw(name data)],
key_name => 'registrantPublicKey',
},
update_name => {
url => 'names/update',
required => [qw(newName newData)],
key_name => 'ownerPublicKey',
},
# reward-shares
reward_share => {
url => 'addresses/rewardshare',
required => [qw(recipient rewardSharePublicKey sharePercent)],
key_name => 'minterPublicKey',
},
# arbitrary
arbitrary => {
url => 'arbitrary',
required => [qw(service dataType data)],
key_name => 'senderPublicKey',
},
# chat
chat => {
url => 'chat',
required => [qw(data)],
optional => [qw(recipient isText isEncrypted)],
key_name => 'senderPublicKey',
defaults => { isText => 'true' },
pow_url => 'chat/compute',
},
# misc
publicize => {
url => 'addresses/publicize',
required => [],
key_name => 'senderPublicKey',
pow_url => 'addresses/publicize/compute',
},
# Cross-chain trading
build_trade => {
url => 'crosschain/build',
required => [qw(initialQortAmount finalQortAmount fundingQortAmount secretHash bitcoinAmount)],
optional => [qw(tradeTimeout)],
key_name => 'creatorPublicKey',
defaults => { tradeTimeout => 10800 },
},
trade_recipient => {
url => 'crosschain/tradeoffer/recipient',
required => [qw(atAddress recipient)],
key_name => 'creatorPublicKey',
remove => [qw(timestamp reference fee)],
},
trade_secret => {
url => 'crosschain/tradeoffer/secret',
required => [qw(atAddress secret)],
key_name => 'recipientPublicKey',
remove => [qw(timestamp reference fee)],
},
# These are fake transaction types to provide utility functions:
sign => {
url => 'transactions/sign',
required => [qw{transactionBytes}],
},
);
my $tx_type = lc(shift(@ARGV));
if ($tx_type eq 'all') {
printf STDERR "Transaction types: %s\n", join(', ', sort { $a cmp $b } keys %TRANSACTION_TYPES);
exit 2;
}
my $tx_info = $TRANSACTION_TYPES{$tx_type};
if (!$tx_info) {
printf STDERR "Transaction type '%s' unknown\n", uc($tx_type);
exit 1;
}
my @required = @{$tx_info->{required}};
if (@ARGV < @required + 1) {
printf STDERR "usage: %s %s <privkey> %s", $proc, uc($tx_type), join(' ', map { "<$_>"} @required);
printf STDERR " %s", join(' ', map { "[$_ <$_>]" } @{$tx_info->{optional}}) if exists $tx_info->{optional};
print "\n";
exit 2;
}
my $priv_key = shift @ARGV;
my $account = account($priv_key);
my $raw;
if ($tx_type ne 'sign') {
my %extras;
foreach my $required_arg (@required) {
$extras{$required_arg} = shift @ARGV;
}
# For CHAT we use a random reference
if ($tx_type eq 'chat') {
$extras{reference} = api('utils/random?length=64');
}
%extras = (%extras, %{$tx_info->{defaults}}) if exists $tx_info->{defaults};
%extras = (%extras, @ARGV);
$raw = build_raw($tx_type, $account, %extras);
printf "Raw: %s\n", $raw if $opt{d} || (!$opt{s} && !$opt{p});
# Some transaction types require proof-of-work, e.g. CHAT
if (exists $tx_info->{pow_url}) {
$raw = api($tx_info->{pow_url}, $raw);
printf "Raw with PoW: %s\n", $raw if $opt{d};
}
} else {
$raw = shift @ARGV;
$opt{s}++;
}
if ($opt{s}) {
my $signed = sign($account->{private}, $raw);
printf "Signed: %s\n", $signed if $opt{d} || $tx_type eq 'sign';
if ($opt{p}) {
my $processed = process($signed);
printf "Processed: %s\n", $processed if $opt{d};
}
my $hex = api('utils/frombase58', $signed);
# sig is last 64 bytes / 128 chars
my $sighex = substr($hex, -128);
my $sig58 = api('utils/tobase58/{hex}', '', '{hex}', $sighex);
printf "Signature: %s\n", $sig58;
}
sub account {
my ($creator) = @_;
my $account = { private => $creator };
$account->{public} = api('utils/publickey', $creator);
$account->{address} = api('addresses/convert/{publickey}', '', '{publickey}', $account->{public});
return $account;
}
sub build_raw {
my ($type, $account, %extras) = @_;
my $tx_info = $TRANSACTION_TYPES{$type};
die("unknown tx type: $type\n") unless defined $tx_info;
my $ref = exists $extras{reference} ? $extras{reference} : lastref($account->{address});
my %json = (
timestamp => time * 1000,
reference => $ref,
fee => $DEFAULT_FEE,
);
$json{$tx_info->{key_name}} = $account->{public} if exists $tx_info->{key_name};
foreach my $required (@{$tx_info->{required}}) {
die("missing tx field: $required\n") unless exists $extras{$required};
}
while (my ($key, $value) = each %extras) {
$json{$key} = $value;
}
if (exists $tx_info->{remove}) {
foreach my $key (@{$tx_info->{remove}}) {
delete $json{$key};
}
}
my $json = "{\n";
while (my ($key, $value) = each %json) {
if (ref($value) eq 'ARRAY') {
$json .= "\t\"$key\": [],\n";
} else {
$json .= "\t\"$key\": \"$value\",\n";
}
}
# remove final comma
substr($json, -2, 1) = '';
$json .= "}\n";
printf "%s:\n%s\n", $type, $json if $opt{d};
my $raw = api($tx_info->{url}, $json);
return $raw;
}
sub sign {
my ($private, $raw) = @_;
my $json = <<" __JSON__";
{
"privateKey": "$private",
"transactionBytes": "$raw"
}
__JSON__
return api('transactions/sign', $json);
}
sub process {
my ($signed) = @_;
return api('transactions/process', $signed);
}
sub lastref {
my ($address) = @_;
return api('addresses/lastreference/{address}', '', '{address}', $address)
}
sub api {
my ($endpoint, $postdata, @args) = @_;
my $url = $endpoint;
my $method = 'GET';
for (my $i = 0; $i < @args; $i += 2) {
my $placemarker = $args[$i];
my $value = $args[$i + 1];
$url =~ s/$placemarker/$value/g;
}
my $curl = "curl --silent --output - --url '$BASE_URL/$url'";
if (defined $postdata && $postdata ne '') {
$postdata =~ tr|\n| |s;
$curl .= " --header 'Content-Type: application/json' --data-binary '$postdata'";
$method = 'POST';
}
my $response = `$curl 2>/dev/null`;
chomp $response;
if ($response eq '' || substr($response, 0, 6) eq '<html>' || $response =~ m/(^\{|,)"error":(\d+)[,}]/) {
die("API call '$method $BASE_URL/$endpoint' failed:\n$response\n");
}
return $response;
}