diff --git a/lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar b/lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar index 05c548c8..20e773a4 100644 Binary files a/lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar and b/lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar differ diff --git a/lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom b/lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom index 16f644b9..d88a53e2 100644 --- a/lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom +++ b/lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom @@ -10,14 +10,13 @@ UTF-8 false - - 3.8.1 - 3.2.0 - 3.3.1 - 3.0.0-M4 - 3.2.0 - - 1.64 + 1.69 + 4.13.2 + 3.11.0 + 3.3.0 + 3.6.3 + 3.3.0 + 3.2.2 @@ -117,7 +116,7 @@ junit junit - 4.13 + ${junit.version} test diff --git a/lib/org/ciyam/AT/maven-metadata-local.xml b/lib/org/ciyam/AT/maven-metadata-local.xml index d8f3dd34..355c973f 100644 --- a/lib/org/ciyam/AT/maven-metadata-local.xml +++ b/lib/org/ciyam/AT/maven-metadata-local.xml @@ -5,14 +5,11 @@ 1.4.1 - 1.3.4 - 1.3.5 - 1.3.6 1.3.7 1.3.8 1.4.0 1.4.1 - 20230821074325 + 20231212092227 diff --git a/pom.xml b/pom.xml index b64917f4..4d736704 100644 --- a/pom.xml +++ b/pom.xml @@ -10,18 +10,18 @@ 7dc8c6f 0.15.10 1.69 - 3.4.0 + 3.5.0 ${maven.build.timestamp} 1.4.1 3.8.0 1.11.0 2.11.0 - 1.24.0 - 3.13.0 + 1.25.0 + 3.14.0 1.2.2 0.12.3 4.9.10 - 1.59.0 + 1.60.0 32.1.3-jre 2.2 1.2.1 @@ -34,7 +34,7 @@ 9.4.53.v20231009 1.1.1 20231013 - 1.16.2 + 1.17.1 5.10.0 1.0.0 2.21.1 @@ -46,15 +46,15 @@ 3.2.2 1.1.0 UTF-8 - 3.24.4 + 3.25.0 1.5.3 0.16 1.17 1.7.36 2.0.10 - 5.9.0 + 5.10.3 1.2 - 2.16.1 + 2.16.2 1.9 diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index 93dac568..276116fc 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -522,6 +522,10 @@ public class QortalATAPI extends API { /** Returns AT account's lastReference */ private byte[] getLastReference() { + // If we have transactions already, then use signature from last transaction + if (!this.transactions.isEmpty()) + return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature(); + try { // Look up AT's account's last reference from repository Account atAccount = this.getATAccount(); diff --git a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java index f14efae8..3643e552 100644 --- a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java +++ b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java @@ -1,33 +1,33 @@ package org.qortal.crypto; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; +import javax.net.ssl.*; import java.security.cert.X509Certificate; public abstract class TrustlessSSLSocketFactory { - // Create a trust manager that does not validate certificate chains + /** + * Creates a SSLSocketFactory that ignore certificate chain validation because ElectrumX servers use mostly + * self signed certificates. + */ private static final TrustManager[] TRUSTLESS_MANAGER = new TrustManager[] { new X509TrustManager() { - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; + public X509Certificate[] getAcceptedIssuers() { + return null; } - - public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { + public void checkClientTrusted(X509Certificate[] certs, String authType) { } - - public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { + public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; - // Install the all-trusting trust manager + /** + * Install the all-trusting trust manager. + */ private static final SSLContext sc; static { try { - sc = SSLContext.getInstance("TLSv1.3"); + sc = SSLContext.getInstance("SSL"); sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom()); } catch (Exception e) { throw new RuntimeException(e); @@ -37,5 +37,4 @@ public abstract class TrustlessSSLSocketFactory { public static SSLSocketFactory getSocketFactory() { return sc.getSocketFactory(); } - } diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index ba90208b..29c726b1 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -221,7 +221,7 @@ public class Settings { public long recoveryModeTimeout = 9999999999999L; /** Minimum peer version number required in order to sync with them */ - private String minPeerVersion = "4.3.2"; + private String minPeerVersion = "4.4.0"; /** Whether to allow connections with peers below minPeerVersion * If true, we won't sync with them but they can still sync with us, and will show in the peers list * If false, sync will be blocked both ways, and they will not appear in the peers list */ diff --git a/src/main/resources/i18n/ApiError_he.properties b/src/main/resources/i18n/ApiError_he.properties new file mode 100644 index 00000000..5ce597f4 --- /dev/null +++ b/src/main/resources/i18n/ApiError_he.properties @@ -0,0 +1,83 @@ +#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) +# Keys are from api.ApiError enum + +# "localeLang": "he", + +### Common ### +JSON = נכשל בניתוח הודעת JSON + +INSUFFICIENT_BALANCE = יתרה לא מספקת + +UNAUTHORIZED = קריאת API לא מורשית + +REPOSITORY_ISSUE = שגיאת מאגר + +NON_PRODUCTION = קריאת API זו אינה מותרת עבור מערכות ייצור + +BLOCKCHAIN_NEEDS_SYNC = הבלוקצ'יין צריך להסתנכרן תחילה + +NO_TIME_SYNC = עדיין אין סנכרון שעון + +### Validation ### +INVALID_SIGNATURE = חתימה לא חוקית + +INVALID_ADDRESS = כתובת לא חוקית + +INVALID_PUBLIC_KEY = מפתח ציבורי לא חוקי + +INVALID_DATA = נתונים לא חוקיים + +INVALID_NETWORK_ADDRESS = כתובת רשת לא חוקית + +ADDRESS_UNKNOWN = כתובת חשבון לא ידועה + +INVALID_CRITERIA = קריטריוני חיפוש לא חוקיים + +INVALID_REFERENCE = הפניה לא חוקית + +TRANSFORMATION_ERROR = לא הצליח להפוך את JSON לעסקה + +INVALID_PRIVATE_KEY = מפתח פרטי לא חוקי + +INVALID_HEIGHT = גובה בלוק לא חוקי + +CANNOT_MINT = החשבון לא יכול להטביע + +### Blocks ### +BLOCK_UNKNOWN = בלוק לא ידוע + +### Transactions ### +TRANSACTION_UNKNOWN = עסקה לא ידועה + +PUBLIC_KEY_NOT_FOUND = מפתח ציבורי לא נמצא + +# this one is special in that caller expected to pass two additional strings, hence the two %s +TRANSACTION_INVALID = עסקה לא חוקית: %s (%s) + +### Naming ### +NAME_UNKNOWN = שם לא ידוע + +### Asset ### +INVALID_ASSET_ID = מזהה נכס לא חוקי + +INVALID_ORDER_ID = מזהה הזמנת נכס לא חוקי + +ORDER_UNKNOWN = מזהה הזמנת נכס לא ידוע + +### Groups ### +GROUP_UNKNOWN = קבוצה לא ידועה + +### Foreign Blockchain ### +FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = בעיה זרה בלוקצ'יין או ElectrumX ברשת + +FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = יתרה לא מספקת בבלוקצ'יין זר + +FOREIGN_BLOCKCHAIN_TOO_SOON = מוקדם מדי לשדר עסקת בלוקצ'יין זרה (זמן נעילה/זמן חסימה חציוני) + +### Trade Portal ### +ORDER_SIZE_TOO_SMALL = כמות ההזמנה נמוכה מדי + +### Data ### +FILE_NOT_FOUND = הקובץ לא נמצא + +NO_REPLY = עמית לא השיב בזמן המותר diff --git a/src/main/resources/i18n/SysTray_he.properties b/src/main/resources/i18n/SysTray_he.properties new file mode 100644 index 00000000..50ef8933 --- /dev/null +++ b/src/main/resources/i18n/SysTray_he.properties @@ -0,0 +1,48 @@ +#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) +# SysTray pop-up menu + +APPLYING_UPDATE_AND_RESTARTING = מחיל עדכון אוטומטי ומפעיל מחדש... + +AUTO_UPDATE = עדכון אוטומטי + +BLOCK_HEIGHT = גובה + +BLOCKS_REMAINING = נותרו בלוקים + +BUILD_VERSION = גרסת בנייה + +CHECK_TIME_ACCURACY = בדוק את דיוק הזמן + +CONNECTING = מתחבר + +CONNECTION = חיבור + +CONNECTIONS = חיבורים + +CREATING_BACKUP_OF_DB_FILES = יוצר גיבוי של קבצי מסד נתונים... + +DB_BACKUP = גיבוי מסד נתונים + +DB_CHECKPOINT = נקודת ביקורת של מסד נתונים + +DB_MAINTENANCE = תחזוקת מסד נתונים + +EXIT = יציאה + +LITE_NODE = Lite Node + +MINTING_DISABLED = כרייה מבוטלת + +MINTING_ENABLED = \u2714 הטבעה + +OPEN_UI = ממשק משתמש פתוח + +PERFORMING_DB_CHECKPOINT = שומר שינויים לא מחויבים במסד הנתונים... + +PERFORMING_DB_MAINTENANCE = מבצע תחזוקה מתוזמנת... + +SYNCHRONIZE_CLOCK = סנכרן שעון + +SYNCHRONIZING_BLOCKCHAIN ​​= מסנכרן + +SYNCHRONIZING_CLOCK = מסנכרן שעון diff --git a/src/main/resources/i18n/TransactionValidity_he.properties b/src/main/resources/i18n/TransactionValidity_he.properties new file mode 100644 index 00000000..889d7b78 --- /dev/null +++ b/src/main/resources/i18n/TransactionValidity_he.properties @@ -0,0 +1,195 @@ +# + +ACCOUNT_ALREADY_EXISTS = חשבון כבר קיים + +ACCOUNT_CANNOT_REWARD_SHARE = ​​חשבון לא יכול לחלוק תגמולים + +ADDRESS_ABOVE_RATE_LIMIT = הכתובת הגיעה למגבלת התעריף שצוינה + +ADDRESS_BLOCKED = כתובת זו חסומה + +ALREADY_GROUP_ADMIN = כבר מנהל קבוצה + +ALREADY_GROUP_MEMBER = כבר חבר בקבוצה + +ALREADY_VOTED_FOR_THAT_OPTION = כבר הצביע עבור אפשרות זו + +ASSET_ALREADY_EXISTS = הנכס כבר קיים + +ASSET_DOES_NOT_EXIST = הנכס אינו קיים + +ASSET_DOES_NOT_MATCH_AT = הנכס אינו תואם לנכס של AT + +ASSET_NOT_SPENDABLE = הנכס אינו ניתן לבזבז + +AT_ALREADY_EXISTS = AT כבר קיים + +AT_IS_FINISHED = AT הסתיים + +AT_UNKNOWN = AT לא ידוע + +BAN_EXISTS = החסימה כבר קיימת + +BAN_UNKNOWN = איסור לא ידוע + +BANNED_FROM_GROUP = חסום מהקבוצה + +BUYER_ALREADY_OWNER = הקונה כבר הבעלים + +CLOCK_NOT_SYNCED = שעון לא מסונכרן + +DUPLICATE_MESSAGE = כתובת שנשלחה הודעה כפולה + +DUPLICATE_OPTION = אפשרות שכפול + +GROUP_ALREADY_EXISTS = הקבוצה כבר קיימת + +GROUP_APPROVAL_DECIDED = אישור הקבוצה כבר הוחלט + +GROUP_APPROVAL_NOT_REQUIRED = אין צורך באישור קבוצתי + +GROUP_DOES_NOT_EXIST = קבוצה לא קיימת + +GROUP_ID_MISMATCH = אי התאמה של מזהה הקבוצה + +GROUP_OWNER_CANNOT_LEAVE = בעל הקבוצה לא יכול לעזוב את הקבוצה + +HAVE_EQUALS_WANT = have-asset זהה ל-want-asset + +INCORRECT_NONCE = לא תקין של PoW + +INSUFFICIENT_FEE = עמלה לא מספקת + +INVALID_ADDRESS = כתובת לא חוקית + +INVALID_AMOUNT = סכום לא חוקי + +INVALID_ASSET_OWNER = בעל נכס לא חוקי + +INVALID_AT_TRANSACTION = עסקת AT לא חוקית + +INVALID_AT_TYPE_LENGTH = אורך AT 'סוג' לא חוקי + +INVALID_BUT_OK = לא חוקי אבל בסדר + +INVALID_CREATION_BYTES = בתים לא חוקיים של יצירה + +INVALID_DATA_LENGTH = אורך נתונים לא חוקי + +INVALID_DESCRIPTION_LENGTH = אורך תיאור לא חוקי + +INVALID_GROUP_APPROVAL_THRESHOLD = סף לא חוקי לאישור קבוצה + +INVALID_GROUP_BLOCK_DELAY = עיכוב חסימת אישור קבוצה לא חוקי + +INVALID_GROUP_ID = מזהה קבוצה לא חוקי + +INVALID_GROUP_OWNER = בעל קבוצה לא חוקי + +INVALID_LIFETIME = משך חיים לא חוקי + +INVALID_NAME_LENGTH = אורך שם לא חוקי + +INVALID_NAME_OWNER = בעל שם לא חוקי + +INVALID_OPTION_LENGTH = אורך אפשרויות לא חוקי + +INVALID_OPTIONS_COUNT = ספירת אפשרויות לא חוקיות + +INVALID_ORDER_CREATOR = יוצר הזמנה לא חוקי + +INVALID_PAYMENTS_COUNT = ספירת תשלומים לא חוקיים + +INVALID_PUBLIC_KEY = מפתח ציבורי לא חוקי + +INVALID_QUANTITY = כמות לא חוקית + +INVALID_REFERENCE = הפניה לא חוקית + +INVALID_RETURN = החזרה לא חוקית + +INVALID_REWARD_SHARE_PERCENT = אחוז חלוקת תגמולים לא חוקי + +INVALID_SELLER = מוכר לא חוקי + +INVALID_TAGS_LENGTH = אורך 'תגים' לא חוקי + +INVALID_TIMESTAMP_SIGNATURE = חתימת חותמת זמן לא חוקית + +INVALID_TX_GROUP_ID = מזהה קבוצת עסקאות לא חוקי + +INVALID_VALUE_LENGTH = אורך 'ערך' לא חוקי + +INVITE_UNKNOWN = הזמנה לקבוצה לא ידועה + +JOIN_REQUEST_EXISTS = בקשת הצטרפות לקבוצה כבר קיימת + +MAXIMUM_REWARD_SHARES = כבר במספר המרבי של שיתופי תגמול עבור חשבון זה + +MISSING_CREATOR = חסר יוצר + +MULTIPLE_NAMES_FORBIDDEN = אסור להשתמש במספר שמות רשומים לכל חשבון + +NAME_ALREADY_FOR_SALE = שם כבר למכירה + +NAME_ALREADY_REGISTERED = השם כבר רשום + +NAME_BLOCKED = השם הזה חסום + +NAME_DOES_NOT_EXIST = שם לא קיים + +NAME_NOT_FOR_SALE = השם אינו למכירה + +NAME_NOT_NORMALIZED = שם לא בצורת Unicode 'מנורמלת' + +NEGATIVE_AMOUNT = סכום לא חוקי/שלילי + +NEGATIVE_FEE = עמלה לא חוקית/שלילית + +NEGATIVE_PRICE = מחיר לא חוקי/שלילי + +NO_BALANCE = איזון לא מספיק + +NO_BLOCKCHAIN_LOCK = הבלוקצ'יין של הצומת תפוס כעת + +NO_FLAG_PERMISSION = לחשבון אין הרשאה זו + +NOT_GROUP_ADMIN = החשבון אינו מנהל קבוצה + +NOT_GROUP_MEMBER = החשבון אינו חבר בקבוצה + +NOT_MINTING_ACCOUNT = החשבון אינו יכול להטביע + +NOT_YET_RELEASED = תכונה עדיין לא שוחררה + +OK = בסדר + +ORDER_ALREADY_CLOSED = הזמנת סחר בנכס כבר סגורה + +ORDER_DOES_NOT_EXIST = הוראת סחר בנכס לא קיימת + +POLL_ALREADY_EXISTS = סקר כבר קיים + +POLL_DOES_NOT_EXIST = סקר אינו קיים + +POLL_OPTION_DOES_NOT_EXIST = אפשרות סקר לא קיימת + +PUBLIC_KEY_UNKNOWN = מפתח ציבורי לא ידוע + +REWARD_SHARE_UNKNOWN = חלוקת פרס לא ידוע + +SELF_SHARE_EXISTS = שיתוף עצמי (שיתוף תגמול) כבר קיים + +TIMESTAMP_TOO_NEW = חותמת זמן חדשה מדי + +TIMESTAMP_TOO_OLD = חותמת זמן ישנה מדי + +TOO_MANY_UNCONFIRMED = בחשבון יש יותר מדי עסקאות לא מאושרות בהמתנה + +TRANSACTION_ALREADY_CONFIRMED = העסקה כבר אושרה + +TRANSACTION_ALREADY_EXISTS = עסקה כבר קיימת + +TRANSACTION_UNKNOWN = עסקה לא ידועה + +TX_GROUP_ID_MISMATCH = מזהה הקבוצה של העסקה אינו תואם diff --git a/src/test/java/org/qortal/test/at/CrowdfundTests.java b/src/test/java/org/qortal/test/at/CrowdfundTests.java new file mode 100644 index 00000000..6a60c1ab --- /dev/null +++ b/src/test/java/org/qortal/test/at/CrowdfundTests.java @@ -0,0 +1,425 @@ +package org.qortal.test.at; + +import org.ciyam.at.*; +import org.junit.After; +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.at.AT; +import org.qortal.block.Block; +import org.qortal.data.at.ATData; +import org.qortal.data.at.ATStateData; +import org.qortal.data.block.BlockData; +import org.qortal.data.transaction.*; +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.*; +import org.qortal.transaction.*; +import org.qortal.utils.*; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +public class CrowdfundTests extends Common { + + /* + "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" + "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" + "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" + "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" + */ + + private static final String aliceAddress = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v"; + private static final String bobAddress = "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK"; + private static final String chloeAddress = "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL"; + private static final String dilbertAddress = "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er"; + + // Creation bytes from: java -cp 'target/qrowdfund-1.0.0.jar:target/dependency/*' org.qortal.at.qrowdfund.Qrowdfund 50 12385 Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er + private static final String creationBytes58 = "1Pub6o13xyqfCZj8BMzmXsREVJR6h4xxpS2VPV1R2QwjP78r2ozxsNuvb28GWrT8FoTTQMGnVP7pNii6auUqYr2uunWfcxwhERbDgFdsJqtrJMpQNGB9GerAXYyiFiij35cP6eHw7BmALb3viT6VzqaXX9YB25iztekV5cTreJg7o2hRpFc9Rv8Z9dFXcD1Mm4WCaMaknUgchDi7qDnHA7JX8bn9EFD4WMG5nZHMsrmeqBHirURXr2dMxFprTBo187zztmw7izbv5KzMFP8aRP9uEqdTMhZJmvKqhapMK9UJkxMve3KnsxKn5yyaAeiZ4i9GNfrkjpz5T1VGomUaDmeatNti1bjQ2pwtcgZfFFbrnBFMU2kvcPx1UR53dArtRS7pFbNr3EFwnw2Yiu2xS3Z"; + private static final byte[] creationBytes = Base58.decode(creationBytes58); + private static final long fundingAmount = 2_00000000L; + private static final long SLEEP_PERIOD = 50L; + + private Repository repository = null; + private PrivateKeyAccount deployer; + private DeployAtTransaction deployAtTransaction; + private Account atAccount; + private String atAddress; + private byte[] rawLastTxnTimestamp = new byte[8]; + private Transaction transaction; + + @Before + public void before() throws DataException { + Common.useDefaultSettings(); + + this.repository = RepositoryManager.getRepository(); + this.deployer = Common.getTestAccount(repository, "alice"); + + this.deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + this.atAccount = deployAtTransaction.getATAccount(); + this.atAddress = deployAtTransaction.getATAccount().getAddress(); + } + + @After + public void after() throws DataException { + if (this.repository != null) + this.repository.close(); + + this.repository = null; + } + + @Test + public void testDeploy() throws DataException { + // Confirm initial value is zero + extractLastTxTimestamp(repository, atAddress, rawLastTxnTimestamp); + assertArrayEquals(new byte[8], rawLastTxnTimestamp); + } + + @Test + public void testThresholdNotMet() throws DataException { + // AT deployment in block 2 + + // Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT + BlockUtils.mintBlock(repository); // height now 3 + + // Fetch AT's balance for this height + long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT); + + // Fetch AT's initial lastTxnTimestamp + byte[] creationTimestamp = new byte[8]; + extractLastTxTimestamp(repository, atAddress, creationTimestamp); + + // Mint several blocks + int i = repository.getBlockRepository().getBlockchainHeight(); + long WAKE_HEIGHT = i + SLEEP_PERIOD; + for (; i < WAKE_HEIGHT; ++i) + BlockUtils.mintBlock(repository); + + // We should now be at WAKE_HEIGHT + long height = repository.getBlockRepository().getBlockchainHeight(); + assertEquals(WAKE_HEIGHT, height); + + // AT should have woken and run at this height so balance should have changed + + // Fetch new AT balance + long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT); + + assertNotSame(preMintBalance, postMintBalance); + + // Confirm AT has found no payments + extractLastTxTimestamp(repository, atAddress, rawLastTxnTimestamp); + assertArrayEquals(creationTimestamp, rawLastTxnTimestamp); + + // AT should have finished + ATData atData = repository.getATRepository().fromATAddress(atAddress); + assertTrue(atData.getIsFinished()); + + // AT should have sent balance back to creator + BlockData blockData = repository.getBlockRepository().getLastBlock(); + Block block = new Block(repository, blockData); + List transactions = block.getTransactions(); + + assertEquals(1, transactions.size()); + + Transaction transaction = transactions.get(0); + AtTransaction atTransaction = (AtTransaction) transaction; + assertEquals(aliceAddress, atTransaction.getRecipient().getAddress()); + } + + @Test + public void testThresholdNotMetWithOrphanage() throws DataException { + // AT deployment in block 2 + + // Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT + BlockUtils.mintBlock(repository); // height now 3 + + // Fetch AT's initial lastTxnTimestamp + byte[] creationTimestamp = new byte[8]; + extractLastTxTimestamp(repository, atAddress, creationTimestamp); + + // Mint several blocks + int i = repository.getBlockRepository().getBlockchainHeight(); + long WAKE_HEIGHT = i + SLEEP_PERIOD; + for (; i < WAKE_HEIGHT; ++i) + BlockUtils.mintBlock(repository); + + // AT should have finished + ATData atData = repository.getATRepository().fromATAddress(atAddress); + assertTrue(atData.getIsFinished()); + + // Orphan + BlockUtils.orphanBlocks(repository, 3); + + // Mint several blocks + for (i = 0; i < 3; ++i) + BlockUtils.mintBlock(repository); + + // Confirm AT has found no payments + extractLastTxTimestamp(repository, atAddress, rawLastTxnTimestamp); + assertArrayEquals(creationTimestamp, rawLastTxnTimestamp); + + // AT should have finished + atData = repository.getATRepository().fromATAddress(atAddress); + assertTrue(atData.getIsFinished()); + + // AT should have sent balance back to creator + BlockData blockData = repository.getBlockRepository().getLastBlock(); + Block block = new Block(repository, blockData); + List transactions = block.getTransactions(); + + assertEquals(1, transactions.size()); + + Transaction transaction = transactions.get(0); + AtTransaction atTransaction = (AtTransaction) transaction; + assertEquals(aliceAddress, atTransaction.getRecipient().getAddress()); + } + + @Test + public void testThresholdNotMetWithPaymentsAndRefunds() throws DataException { + // AT deployment in block 2 + + // Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT + BlockUtils.mintBlock(repository); // height now 3 + + // Fetch AT's balance for this height + long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT); + + // Fetch AT's initial lastTxnTimestamp + byte[] creationTimestamp = new byte[8]; + extractLastTxTimestamp(repository, atAddress, creationTimestamp); + + int i = repository.getBlockRepository().getBlockchainHeight(); + long WAKE_HEIGHT = i + SLEEP_PERIOD; + + // Create some test accounts, based on donations + List> donations = List.of( + new Pair<>("QRt11DVBnLaSDxr2KHvx92LdPrjhbhJtkj", 500L), + new Pair<>("QRv7tHnaEpRtfovbTJqkJFmtnoahJrbPGg", 250L), + new Pair<>("QRv7tHnaEpRtfovbTJqkJFmtnoahJrbPGg", 250L), + new Pair<>("QczG8GXU5vPQLTZsJBASQd3fAKJzKwnubv", 250L), + new Pair<>("QNuYHyW4HJn7v3dYUxoTLiyS5tpGQAguMJ", 20L), + new Pair<>("QgVqcSZZ6HRhBvdUmpTvEonaQaH2oWfe58", 500L), + new Pair<>("QfDaxmD8jKi3TovWA1NA8RL5rWYXRC12uX", 10L), + new Pair<>("QSohMWUphRwtEuwAZKqoy8UGS13tk1bBDm", 15L), + new Pair<>("QiNKXRfnX9mTodSed1yRQexhL1HA42RHHo", 420L), + new Pair<>("Qgfh143pRJyxpS92JoazjXNMH1uZueQBZ2", 100L), + new Pair<>("Qgfh143pRJyxpS92JoazjXNMH1uZueQBZ2", 100L) + ); + Map donors = donations.stream() + .map(donation -> donation.getA()) + .distinct() + .collect(Collectors.toMap(name -> name, name -> generateTestAccount(repository, name))); + + // Give donors some QORT so they can donate + donors.values() + .stream() + .forEach(donorAccount -> { + try { + AccountUtils.pay(repository, Common.getTestAccount(repository, "alice"), donorAccount.getAddress(), 2000_00000000L); + } catch (DataException e) { + fail(e.getMessage()); + } + }); + + // Record balances + Map initialDonorBalances = donors.values() + .stream() + .collect(Collectors.toMap(account -> account.getAddress(), account -> { + try { + return account.getConfirmedBalance(Asset.QORT); + } catch (DataException e) { + fail(e.getMessage()); + return null; + } + })); + + // Now make donations + donations.stream() + .forEach(donation -> { + TestAccount donorAccount = donors.get(donation.getA()); + try { + AccountUtils.pay(repository, donorAccount, atAddress, donation.getB() * 1_00000000L); + System.out.printf("AT balance at height %d is %s\n", repository.getBlockRepository().getBlockchainHeight(), Amounts.prettyAmount(atAccount.getConfirmedBalance(Asset.QORT))); + } catch (DataException e) { + fail(e.getMessage()); + } + }); + + // Mint several blocks + i = repository.getBlockRepository().getBlockchainHeight(); + for (; i < WAKE_HEIGHT; ++i) { + BlockUtils.mintBlock(repository); + System.out.printf("AT balance at height %d is %s\n", repository.getBlockRepository().getBlockchainHeight(), Amounts.prettyAmount(atAccount.getConfirmedBalance(Asset.QORT))); + } + + // We should now be at WAKE_HEIGHT + long height = repository.getBlockRepository().getBlockchainHeight(); + assertEquals(WAKE_HEIGHT, height); + + // AT should have woken and run at this height so balance should have changed + System.out.printf("AT balance at height %d is %s\n", repository.getBlockRepository().getBlockchainHeight(), Amounts.prettyAmount(atAccount.getConfirmedBalance(Asset.QORT))); + + // Fetch new AT balance + long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT); + + assertNotSame(preMintBalance, postMintBalance); + + + // Payments might happen over multiple blocks! + Map expectedBalances = new HashMap<>(initialDonorBalances); + + ATData atData; + do { + // Confirm AT has found payments + extractLastTxTimestamp(repository, atAddress, rawLastTxnTimestamp); + assertNotSame(ByteArray.wrap(creationTimestamp), ByteArray.copyOf(rawLastTxnTimestamp)); + + // AT should have sent refunds + BlockData blockData = repository.getBlockRepository().getLastBlock(); + Block block = new Block(repository, blockData); + List transactions = block.getTransactions(); + + assertNotSame(0, transactions.size()); + + // Compute expected balances + for (var transaction : transactions) { + AtTransaction atTransaction = (AtTransaction) transaction; + ATTransactionData atTransactionData = (ATTransactionData) atTransaction.getTransactionData(); + String recipient = atTransactionData.getRecipient(); + + // Skip if this is a refund to AT deployer + if (recipient.equals(aliceAddress)) + continue; + + String donorName = donors.entrySet() + .stream() + .filter(donor -> donor.getValue().getAddress().equals(recipient)) + .findFirst() + .get() + .getKey(); + System.out.printf("AT paid %s to %s\n", Amounts.prettyAmount(atTransactionData.getAmount()), donorName); + + expectedBalances.compute(atTransactionData.getRecipient(), (key, balance) -> balance - AccountUtils.fee); + } + + // AT should have finished + atData = repository.getATRepository().fromATAddress(atAddress); + + // Mint new block in case we need to loop round again + BlockUtils.mintBlock(repository); + System.out.printf("AT balance at height %d is %s\n", repository.getBlockRepository().getBlockchainHeight(), Amounts.prettyAmount(atAccount.getConfirmedBalance(Asset.QORT))); + } while (!atData.getIsFinished()); + + // Compare expected balances + donors.entrySet() + .forEach(donor -> { + String donorName = donor.getKey(); + TestAccount donorAccount = donor.getValue(); + + Long expectedBalance = expectedBalances.get(donorAccount.getAddress()); + Long actualBalance = null; + try { + actualBalance = donorAccount.getConfirmedBalance(Asset.QORT); + } catch (DataException e) { + fail(e.getMessage()); + } + + assertEquals(expectedBalance, actualBalance); + }); + } + + + 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 extractLastTxTimestamp(Repository repository, String atAddress, byte[] rawLastTxnTimestamp) throws DataException { + // Check AT result + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + byte[] dataBytes = MachineState.extractDataBytes(stateData); + + System.arraycopy(dataBytes, 5 * MachineState.VALUE_SIZE, rawLastTxnTimestamp, 0, rawLastTxnTimestamp.length); + } + + 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 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[] rawLastTxTimestamp = new byte[8]; + extractLastTxTimestamp(repository, atAddress, rawLastTxTimestamp); + + Timestamp expectedTimestamp = new Timestamp(height, sequence); + Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawLastTxTimestamp, 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(rawLastTxTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length); + + assertArrayEquals(expectedPartialSignature, actualPartialSignature); + } + + private static TestAccount generateTestAccount(Repository repository, String accountName) { + byte[] seed = new byte[32]; + new SecureRandom().nextBytes(seed); + return new TestAccount(repository, accountName, Base58.encode(seed), false); + } + +} diff --git a/testnet/testchain.json b/testnet/testchain.json index 089bd693..66287bef 100644 --- a/testnet/testchain.json +++ b/testnet/testchain.json @@ -27,6 +27,9 @@ "onlineAccountsModulusV2Timestamp": 0, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 1692554400000, + "blockRewardBatchStartHeight": 10000, + "blockRewardBatchSize": 1000, + "blockRewardBatchAccountsBlockCount": 25, "rewardsByHeight": [ { "height": 1, "reward": 5.00 }, { "height": 259201, "reward": 4.75 }, @@ -69,8 +72,8 @@ ], "ciyamAtSettings": { "feePerStep": "0.00000001", - "maxStepsPerRound": 500, - "stepsPerFunctionCall": 10, + "maxStepsPerRound": 1000, + "stepsPerFunctionCall": 100, "minutesPerBlock": 1 }, "featureTriggers": { @@ -89,11 +92,11 @@ "selfSponsorshipAlgoV1Height": 9999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 1678622400000 + "arbitraryOptionalFeeTimestamp": 0 }, "genesisInfo": { "version": 4, - "timestamp": "1677572542000", + "timestamp": "1701874800000", "transactions": [ { "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORTAL coin", "quantity": 0, "isDivisible": true, "data": "{}" }, { "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, @@ -2661,4 +2664,4 @@ { "type": "GENESIS", "recipient": "QU7EUWDZz7qJVPih3wL9RKTHRfPFy4ASHC", "amount": 10 } ] } -} \ No newline at end of file +}