This repository has been archived by the owner on Feb 15, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 42
/
ClientTransactionUtil.java
296 lines (251 loc) · 15.3 KB
/
ClientTransactionUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
package co.nyzo.verifier.client;
import co.nyzo.verifier.*;
import co.nyzo.verifier.messages.TransactionResponse;
import co.nyzo.verifier.nyzoString.*;
import co.nyzo.verifier.util.IpUtil;
import co.nyzo.verifier.util.PrintUtil;
import co.nyzo.verifier.util.ThreadUtil;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class ClientTransactionUtil {
public static long suggestedTransactionTimestamp() {
// Check if the blockchain is behind. If so, a timestamp offset can be applied to prevent transaction delay.
Block localFrozenEdge = BlockManager.getFrozenEdge();
long timeSinceFrozenEdgeVerification = System.currentTimeMillis() - localFrozenEdge.getVerificationTimestamp();
long predictedConsensusFrozenEdge = localFrozenEdge.getBlockHeight() + timeSinceFrozenEdgeVerification /
Block.blockDuration;
// The offset is calculated based on the difference between the open edge and predicted consensus edge, with a
// small buffer (3 blocks).
long timestampOffset = Math.max(0L, (BlockManager.openEdgeHeight(false) - predictedConsensusFrozenEdge - 3) *
Block.blockDuration);
System.out.println("open edge: " + BlockManager.openEdgeHeight(false) + ", predicted consensus edge: " +
predictedConsensusFrozenEdge + ", offset: " + timestampOffset);
// Return the timestamp.
return System.currentTimeMillis() - timestampOffset;
}
private static Transaction createTransaction(NyzoStringPrivateSeed signerSeed,
NyzoStringPublicIdentifier receiverIdentifier, byte[] senderData,
long amount) {
long timestamp = suggestedTransactionTimestamp();
Block localFrozenEdge = BlockManager.getFrozenEdge();
return Transaction.standardTransaction(timestamp, amount, receiverIdentifier.getIdentifier(),
localFrozenEdge.getBlockHeight(), localFrozenEdge.getHash(), senderData, signerSeed.getSeed());
}
public static void createAndSendTransaction(NyzoStringPrivateSeed signerSeed,
NyzoStringPublicIdentifier receiverIdentifier, byte[] senderData,
long amount, CommandOutput output) {
createAndSendTransaction(signerSeed, receiverIdentifier, senderData, amount, null, 0, output);
}
public static void createAndSendTransaction(NyzoStringPrivateSeed signerSeed,
NyzoStringPublicIdentifier receiverIdentifier, byte[] senderData,
long amount, byte[] receiverIpAddress, int receiverPort,
CommandOutput output) {
Transaction transaction = createTransaction(signerSeed, receiverIdentifier, senderData, amount);
if (receiverIpAddress == null || ByteUtil.isAllZeros(receiverIpAddress) || receiverPort <= 0) {
sendTransactionToLikelyBlockVerifiers(transaction, true, output);
} else {
sendTransactionToReceiver(transaction, receiverIpAddress, receiverPort, output);
}
}
private static void sendTransactionToReceiver(Transaction transaction, byte[] ipAddressBytes, int port,
CommandOutput output) {
// Attempt to send the transaction to the receiver up to 3 times, stopping when a transaction response is
// received.
AtomicBoolean transactionAccepted = new AtomicBoolean(false);
String ipAddress = IpUtil.addressAsString(ipAddressBytes);
for (int i = 0; i < 3 && !transactionAccepted.get(); i++) {
Message message = new Message(MessageType.Transaction5, transaction);
Message.fetchTcp(ipAddress, port, message, new MessageCallback() {
@Override
public void responseReceived(Message message) {
if (message != null && (message.getContent() instanceof TransactionResponse)) {
TransactionResponse response = (TransactionResponse) message.getContent();
if (response.isAccepted()) {
output.println("transaction accepted by " +
NicknameManager.get(message.getSourceNodeIdentifier()));
transactionAccepted.set(true);
} else {
output.println(ConsoleColor.Red + "transaction not accepted by " +
NicknameManager.get(message.getSourceNodeIdentifier()) +
ConsoleColor.reset);
}
}
}
});
// Wait up to 2 seconds for the next iteration.
for (int j = 0; j < 10 && !transactionAccepted.get(); j++) {
ThreadUtil.sleep(200L);
}
}
// If the transaction was not accepted, print an error.
if (!transactionAccepted.get()) {
output.println(ConsoleColor.Red + "unable to send transaction to " + ipAddress + ConsoleColor.reset);
}
}
public static ByteBuffer[] sendTransactionToLikelyBlockVerifiers(Transaction transaction, boolean waitForBlock,
CommandOutput output) {
// This is an array of size 3. The first position is the verifier one ahead of the expected verifier (block
// height = n - 1). The second position is the expected verifier (block height = n). The third position is one
// behind the expected verifier (block height = n + 1).
ByteBuffer[] verifiers = new ByteBuffer[3];
// Determine the height at which the transaction will be included.
long transactionHeight = BlockManager.heightForTimestamp(transaction.getTimestamp());
// Get the current frozen edge and the current cycle. Using the frozen edge as a reference, the verifier that
// should be expected to verify this block can be determined based on its position in the cycle.
Block frozenEdge = BlockManager.getFrozenEdge();
List<ByteBuffer> currentCycle = BlockManager.verifiersInCurrentCycleList();
int frozenEdgeVerifierIndex = currentCycle.indexOf(ByteBuffer.wrap(frozenEdge.getVerifierIdentifier()));
// Send the transaction to the expected verifier, the previous verifier, and the next verifier.
Set<ByteBuffer> likelyVerifiers = new HashSet<>();
for (int i = -1; i < 2; i++) {
int indexOfVerifier = (int) ((transactionHeight - frozenEdge.getBlockHeight() +
frozenEdgeVerifierIndex + i) % currentCycle.size());
if (indexOfVerifier >= 0 && indexOfVerifier < currentCycle.size()) {
ByteBuffer identifier = currentCycle.get(indexOfVerifier);
likelyVerifiers.add(identifier);
verifiers[i + 1] = identifier;
}
}
for (Node node : ClientNodeManager.getMesh()) {
if (likelyVerifiers.contains(ByteBuffer.wrap(node.getIdentifier()))) {
Message message = new Message(MessageType.Transaction5, transaction);
Message.fetch(node, message, new MessageCallback() {
@Override
public void responseReceived(Message message) {
// Print the transaction response.
if (message == null) {
output.println(ConsoleColor.Red + "transaction response null from " +
NicknameManager.get(node.getIdentifier()) + ", " +
IpUtil.addressAsString(node.getIpAddress()) + ConsoleColor.reset);
} else {
if (message.getContent() instanceof TransactionResponse) {
TransactionResponse response = (TransactionResponse) message.getContent();
if (response.isAccepted()) {
output.println("transaction accepted by " +
NicknameManager.get(message.getSourceNodeIdentifier()));
} else {
output.println(ConsoleColor.Yellow + "transaction not accepted by " +
NicknameManager.get(message.getSourceNodeIdentifier()) + ": " +
response.getMessage() + ConsoleColor.reset);
}
} else {
output.println(ConsoleColor.Red + "transaction response: invalid" + ConsoleColor.reset);
}
}
}
});
}
}
// If indicated, wait for the block that should incorporate the transaction to be received.
if (waitForBlock) {
// Wait for the transaction's block to be received, up to 30 seconds.
int waitCount = 0;
while (BlockManager.getFrozenEdgeHeight() < transactionHeight && waitCount++ < 30) {
ThreadUtil.sleep(1000L);
// Periodically display a "waiting for block" message.
if (waitCount % 5 == 1) {
output.println(ConsoleColor.Yellow.backgroundBright() + "waiting for block..." +
ConsoleColor.reset);
}
}
ThreadUtil.sleep(200L);
// Now, get the block in which the transaction was supposed to be incorporated. Report whether the
// transaction is in the block.
Block transactionBlock = BlockManager.frozenBlockForHeight(transactionHeight);
if (transactionBlock == null) {
System.out.println(ConsoleColor.Red + "unable to determine whether transaction was incorporated into " +
"the chain" + ConsoleColor.reset);
} else {
boolean transactionIsInChain = false;
for (Transaction blockTransaction : transactionBlock.getTransactions()) {
if (ByteUtil.arraysAreEqual(blockTransaction.getSignature(), transaction.getSignature())) {
transactionIsInChain = true;
}
}
if (transactionIsInChain) {
output.println(ConsoleColor.Green + "transaction processed in block " +
transactionBlock.getBlockHeight() + " with transaction signature " +
PrintUtil.compactPrintByteArray(transaction.getSignature()) + ConsoleColor.reset);
} else {
output.println(ConsoleColor.Red + "transaction not processed" + ConsoleColor.reset);
}
}
}
return verifiers;
}
public static String senderDataForDisplay(byte[] senderData) {
// Sender data is stored and handled as a raw array of bytes. Often, this byte array represents a character
// string. If encoding to a UTF-8 character string and back to a byte array produces the original byte array,
// display as a string. Otherwise, display the hex values of the bytes.
String result;
if (senderData == null) {
result = "";
} else {
result = new String(senderData, StandardCharsets.UTF_8);
if (!ByteUtil.arraysAreEqual(senderData, result.getBytes(StandardCharsets.UTF_8))) {
result = ByteUtil.arrayAsStringWithDashes(senderData);
}
}
return result;
}
public static boolean isNormalizedSenderDataString(String string) {
// Start by trimming leading and trailing whitespace. Unlike UTF-8 strings, padding with whitespace is always a
// mistake and need not be respected.
string = string.trim();
// The string is decoded to a byte array and re-encoded to a new normalized sender-data string. This is
// inefficient computationally, but the inefficiency is not a concern, and it improves locality of logic.
String encoded = normalizedSenderDataString(bytesFromNormalizedSenderDataString(string.trim()));
return encoded != null && encoded.toLowerCase().equals(string.toLowerCase());
}
public static byte[] bytesFromNormalizedSenderDataString(String string) {
// Initially, the sender data is null. This will be replaced with the decoded data if the string is correct.
byte[] senderData = null;
// Get the characters.
char[] characters = string.toLowerCase().toCharArray();
if (characters.length == 67 && characters[0] == 'x' && characters[1] == '(' && characters[66] == ')') {
// Get the underscore index to determine the length of the data.
int underscoreIndex = string.indexOf('_');
int dataLength = underscoreIndex < 0 ? FieldByteSize.maximumSenderDataLength : underscoreIndex / 2 - 1;
// Ensure that all characters in the data field are correct. The left section must be all alphanumeric, and
// the right section must be underscores. The string was converted to lowercase.
boolean allAreCorrect = true;
for (int i = 2; i < 66 && allAreCorrect; i++) {
// This could be written more succinctly, but it would be more difficult to read.
if (i < underscoreIndex) {
allAreCorrect = (characters[i] >= '0' && characters[i] <= '9') ||
(characters[i] >= 'a' && characters[i] <= 'f');
} else {
allAreCorrect = characters[i] == '_';
}
}
// If all characters are correct, decode the data. Otherwise, leave the result null to indicate that the
// input is not a valid sender-data string.
if (allAreCorrect) {
senderData = ByteUtil.byteArrayFromHexString(string.substring(2), dataLength);
}
}
return senderData;
}
public static String normalizedSenderDataString(byte[] senderData) {
// This is a special format to allow input of raw hex sender data in various tools. The sender data field is
// a maximum of 32 bytes, so a text string would typically be limited to 32 characters. This is always produced
// as a fixed 67-character string to eliminate any ambiguity for shorter sender data fields.
String result;
if (senderData == null || senderData.length > FieldByteSize.maximumSenderDataLength) {
result = null;
} else {
StringBuilder resultBuilder = new StringBuilder("X(");
resultBuilder.append(ByteUtil.arrayAsStringNoDashes(senderData));
int numberOfPaddingCharacters = (FieldByteSize.maximumSenderDataLength - senderData.length) * 2;
for (int i = 0; i < numberOfPaddingCharacters; i++) {
resultBuilder.append("_");
}
resultBuilder.append(")");
result = resultBuilder.toString();
}
return result;
}
}