diff --git a/README.md b/README.md index c15b672..8868a27 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ while (true) { ### Example: Transactions Listen for transactions sent to the network: [TransactionLoggerExample.java](/examples/TransactionLoggerExample.java). +### Testnet +If you want to connect to a testnet node instead, you need to provide the magic bytes for that like this: +```java +ErgoSocket ergoSocket = new ErgoSocket(address, self, ErgoSocket.TESTNET_MAGIC); +``` + ## Notes Please understand that ergonnection is only a networking library. diff --git a/src/main/java/com/satergo/ergonnection/ErgoSocket.java b/src/main/java/com/satergo/ergonnection/ErgoSocket.java index 3c3b2bd..c595be1 100644 --- a/src/main/java/com/satergo/ergonnection/ErgoSocket.java +++ b/src/main/java/com/satergo/ergonnection/ErgoSocket.java @@ -6,19 +6,17 @@ import com.satergo.ergonnection.records.Peer; import org.bouncycastle.jcajce.provider.digest.Blake2b; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +import java.io.*; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.SocketException; import java.util.Arrays; import java.util.Collections; import java.util.List; public class ErgoSocket extends Socket { - public static final byte[] MAINNET_MAGIC = { 1, 0, 2, 4 }, TESTNET_MAGIC = { 2, 0, 0, 1 }; + public static final byte[] MAINNET_MAGIC = { 1, 0, 2, 4 }, TESTNET_MAGIC = { 2, 0, 2, 3 }; /** * A basic feature set specifying that the state is "utxo", the client is verifying transactions, has no PoPoW suffix, and 1 block is stored. @@ -34,10 +32,10 @@ public class ErgoSocket extends Socket { private final DataInputStream in; /** - * Creates a new socket for the mainnet + * Creates a new socket * * @param address Address to connect to - * @param self A Peer object that represent this client itself (this is the data that will be sent to the target peer) + * @param self A Peer object that represents this client itself (this is the data that will be sent to the target peer) * @param networkMagic The magic bytes of this network, see {@link #MAINNET_MAGIC} and {@link #TESTNET_MAGIC} * @throws IOException socket exception */ @@ -52,7 +50,7 @@ public ErgoSocket(InetSocketAddress address, Peer self, byte[] networkMagic) thr * Creates a new socket for the mainnet * * @param address Address to connect to - * @param self A Peer object that represent this client itself (this is the data that will be sent to the target peer) + * @param self A Peer object that represents this client itself (this is the data that will be sent to the target peer) * @throws IOException socket exception */ public ErgoSocket(InetSocketAddress address, Peer self) throws IOException { @@ -90,15 +88,27 @@ public void send(ProtocolMessage message) throws IOException { } } + /** + * @throws SocketException if the socket is closed, or it was closed while the message was being read + */ public ProtocolMessage acceptMessage() throws IOException { - byte[] magic = in.readNBytes(4); - if (!Arrays.equals(networkMagic, magic)) - throw new IllegalArgumentException("incorrect magic " + Arrays.toString(magic) + " (must be " + Arrays.toString(networkMagic) + ")"); - int code = in.readUnsignedByte(); - int length = in.readInt(); - in.skipNBytes(4); // checksum - byte[] bytes = in.readNBytes(length); - return Protocol.deserializeMessage(code, bytes); + try { + byte[] magic = InternalStreamUtils.readNFully(in, 4); + if (!Arrays.equals(networkMagic, magic)) { + close(); + throw new IllegalArgumentException("Incorrect magic " + Arrays.toString(magic) + " received (must be " + Arrays.toString(networkMagic) + ")"); + } + int code = in.readUnsignedByte(); + int length = in.readInt(); + in.skipNBytes(4); // checksum + byte[] bytes = InternalStreamUtils.readNFully(in, length); + return Protocol.deserializeMessage(code, bytes); + } catch (EOFException e) { + // Receiving this exception is due to the socket being closed, but it will not have + // been marked as such so this method is called + close(); + throw new SocketException("Socket is closed"); + } } public void sendHandshake() throws IOException { diff --git a/src/main/java/com/satergo/ergonnection/InternalStreamUtils.java b/src/main/java/com/satergo/ergonnection/InternalStreamUtils.java new file mode 100644 index 0000000..0acb912 --- /dev/null +++ b/src/main/java/com/satergo/ergonnection/InternalStreamUtils.java @@ -0,0 +1,31 @@ +package com.satergo.ergonnection; + +import java.io.DataInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class InternalStreamUtils { + private InternalStreamUtils() {} + + public static void writeUTF8ByteLen(VLQOutputStream out, String s) throws IOException { + if (s.length() > 255) throw new IllegalArgumentException("too long"); + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + out.write(bytes.length); + out.write(bytes); + } + + public static String readUTF8ByteLen(VLQInputStream in) throws IOException { + int len = in.read(); + byte[] bytes = in.readNFully(len); + return new String(bytes, StandardCharsets.UTF_8); + } + + /** + * @throws java.io.EOFException if this input stream reaches the end before reading all the bytes. + */ + public static byte[] readNFully(DataInputStream in, int length) throws IOException { + byte[] bytes = new byte[length]; + in.readFully(bytes); + return bytes; + } +} diff --git a/src/main/java/com/satergo/ergonnection/StreamUTF8.java b/src/main/java/com/satergo/ergonnection/StreamUTF8.java deleted file mode 100644 index eabebf9..0000000 --- a/src/main/java/com/satergo/ergonnection/StreamUTF8.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.satergo.ergonnection; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; - -public class StreamUTF8 { - private StreamUTF8() {} - - public static void writeByteLen(OutputStream out, String s) throws IOException { - if (s.length() > 255) throw new IllegalArgumentException("too long"); - byte[] bytes = s.getBytes(StandardCharsets.UTF_8); - out.write(bytes.length); - out.write(bytes); - } - - public static String readByteLen(InputStream in) throws IOException { - int len = in.read(); - byte[] bytes = in.readNBytes(len); - return new String(bytes, StandardCharsets.UTF_8); - } -} diff --git a/src/main/java/com/satergo/ergonnection/VLQInputStream.java b/src/main/java/com/satergo/ergonnection/VLQInputStream.java index 78c47a0..a24cb8d 100644 --- a/src/main/java/com/satergo/ergonnection/VLQInputStream.java +++ b/src/main/java/com/satergo/ergonnection/VLQInputStream.java @@ -33,6 +33,12 @@ public void readFully(byte[] b, int off, int len) throws IOException { } } + public byte[] readNFully(int length) throws IOException { + byte[] bytes = new byte[length]; + readFully(bytes); + return bytes; + } + public int skipBytes(int n) throws IOException { int total = 0; int cur; diff --git a/src/main/java/com/satergo/ergonnection/messages/Inv.java b/src/main/java/com/satergo/ergonnection/messages/Inv.java index 334d7bb..bf24f71 100644 --- a/src/main/java/com/satergo/ergonnection/messages/Inv.java +++ b/src/main/java/com/satergo/ergonnection/messages/Inv.java @@ -18,7 +18,7 @@ public static Inv deserialize(VLQInputStream in) throws IOException { int count = (int) in.readUnsignedInt(); ArrayList elements = new ArrayList<>(); for (int i = 0; i < count; i++) { - elements.add(in.readNBytes(32)); + elements.add(in.readNFully(32)); } return new Inv(typeId, Collections.unmodifiableList(elements)); } diff --git a/src/main/java/com/satergo/ergonnection/messages/ModifierRequest.java b/src/main/java/com/satergo/ergonnection/messages/ModifierRequest.java index ec89605..6ba6b6f 100644 --- a/src/main/java/com/satergo/ergonnection/messages/ModifierRequest.java +++ b/src/main/java/com/satergo/ergonnection/messages/ModifierRequest.java @@ -18,7 +18,7 @@ public static ModifierRequest deserialize(VLQInputStream in) throws IOException int count = (int) in.readUnsignedInt(); ArrayList elements = new ArrayList<>(); for (int i = 0; i < count; i++) { - elements.add(in.readNBytes(32)); + elements.add(in.readNFully(32)); } return new ModifierRequest(typeId, Collections.unmodifiableList(elements)); } diff --git a/src/main/java/com/satergo/ergonnection/messages/SyncInfoOld.java b/src/main/java/com/satergo/ergonnection/messages/SyncInfoOld.java index 4704b0e..f6fbdff 100644 --- a/src/main/java/com/satergo/ergonnection/messages/SyncInfoOld.java +++ b/src/main/java/com/satergo/ergonnection/messages/SyncInfoOld.java @@ -17,7 +17,7 @@ public static SyncInfoOld deserialize(VLQInputStream in) throws IOException { int count = in.readUnsignedShort(); ArrayList lastHeaderIDs = new ArrayList<>(); for (int i = 0; i < count; i++) { - lastHeaderIDs.add(in.readNBytes(32)); + lastHeaderIDs.add(in.readNFully(32)); } return new SyncInfoOld(Collections.unmodifiableList(lastHeaderIDs)); } diff --git a/src/main/java/com/satergo/ergonnection/modifiers/ErgoTransaction.java b/src/main/java/com/satergo/ergonnection/modifiers/ErgoTransaction.java index 54d2d64..711f448 100644 --- a/src/main/java/com/satergo/ergonnection/modifiers/ErgoTransaction.java +++ b/src/main/java/com/satergo/ergonnection/modifiers/ErgoTransaction.java @@ -48,14 +48,14 @@ public static ErgoTransaction deserialize(byte[] id, byte[] data) throws IOExcep int dataInputCount = in.readUnsignedShort(); ArrayList dataInputs = new ArrayList<>(); for (int i = 0; i < dataInputCount; i++) { - dataInputs.add(new DataInput(in.readNBytes(32))); + dataInputs.add(new DataInput(in.readNFully(32))); } // parse distinct ids of tokens in transaction outputs int tokensCount = (int) in.readUnsignedInt(); ArrayList tokens = new ArrayList<>(); for (int i = 0; i < tokensCount; i++) { - tokens.add(new TokenId(in.readNBytes(32))); + tokens.add(new TokenId(in.readNFully(32))); } int outputCandidatesCount = in.readUnsignedShort(); diff --git a/src/main/java/com/satergo/ergonnection/modifiers/Header.java b/src/main/java/com/satergo/ergonnection/modifiers/Header.java index 4e338fa..9012f45 100644 --- a/src/main/java/com/satergo/ergonnection/modifiers/Header.java +++ b/src/main/java/com/satergo/ergonnection/modifiers/Header.java @@ -21,16 +21,16 @@ public record Header(byte[] id, byte version, byte[] parentId, byte[] adProofsRo public static Header deserialize(byte[] id, byte[] data) throws IOException { VLQInputStream in = new VLQInputStream(new ByteArrayInputStream(data)); byte version = in.readByte(); - byte[] parentId = in.readNBytes(32); - byte[] adProofsRoot = in.readNBytes(32); - byte[] transactionsRoot = in.readNBytes(32); - byte[] stateRoot = in.readNBytes(33); + byte[] parentId = in.readNFully(32); + byte[] adProofsRoot = in.readNFully(32); + byte[] transactionsRoot = in.readNFully(32); + byte[] stateRoot = in.readNFully(33); long timestamp = in.readUnsignedLong(); - byte[] extensionHash = in.readNBytes(32); - byte[] nBitsBytes = in.readNBytes(4); + byte[] extensionHash = in.readNFully(32); + byte[] nBitsBytes = in.readNFully(4); long nBits = ((nBitsBytes[0] & 0xFFL) << 24) | ((nBitsBytes[1] & 0xFFL) << 16) | ((nBitsBytes[2] & 0xFFL) << 8) | (nBitsBytes[3] & 0xFFL); int height = Math.toIntExact(in.readUnsignedInt()); - byte[] votes = in.readNBytes(3); + byte[] votes = in.readNFully(3); if (version > INITIAL_VERSION) { int newFieldsSize = in.readUnsignedByte(); in.skipNBytes(newFieldsSize); diff --git a/src/main/java/com/satergo/ergonnection/modifiers/data/AutolykosSolution.java b/src/main/java/com/satergo/ergonnection/modifiers/data/AutolykosSolution.java index 0a72fbb..ac34c0c 100644 --- a/src/main/java/com/satergo/ergonnection/modifiers/data/AutolykosSolution.java +++ b/src/main/java/com/satergo/ergonnection/modifiers/data/AutolykosSolution.java @@ -16,15 +16,15 @@ public record AutolykosSolution(Platform.Ecp minerPubKey, Platform.Ecp oneTimePu public static AutolykosSolution deserialize(VLQInputStream in, byte version) throws IOException { if (version == 1) { - Platform.Ecp minerPublicKey = groupElemFromBytes(in.readNBytes(PUBLIC_KEY_LENGTH)); - Platform.Ecp w = groupElemFromBytes(in.readNBytes(PUBLIC_KEY_LENGTH)); - byte[] nonce = in.readNBytes(8); + Platform.Ecp minerPublicKey = groupElemFromBytes(in.readNFully(PUBLIC_KEY_LENGTH)); + Platform.Ecp w = groupElemFromBytes(in.readNFully(PUBLIC_KEY_LENGTH)); + byte[] nonce = in.readNFully(8); int dBytesLength = in.readUnsignedByte(); - BigInteger d = BigIntegers.fromUnsignedByteArray(in.readNBytes(dBytesLength)); + BigInteger d = BigIntegers.fromUnsignedByteArray(in.readNFully(dBytesLength)); return new AutolykosSolution(minerPublicKey, w, nonce, d); } else { - Platform.Ecp minerPublicKey = groupElemFromBytes(in.readNBytes(PUBLIC_KEY_LENGTH)); - byte[] nonce = in.readNBytes(8); + Platform.Ecp minerPublicKey = groupElemFromBytes(in.readNFully(PUBLIC_KEY_LENGTH)); + byte[] nonce = in.readNFully(8); return new AutolykosSolution(minerPublicKey, CryptoConstants.dlogGroup().generator(), nonce, BigInteger.ZERO); } } diff --git a/src/main/java/com/satergo/ergonnection/records/Feature.java b/src/main/java/com/satergo/ergonnection/records/Feature.java index b6ac997..05c6c5e 100644 --- a/src/main/java/com/satergo/ergonnection/records/Feature.java +++ b/src/main/java/com/satergo/ergonnection/records/Feature.java @@ -9,7 +9,7 @@ public record Feature(int id, byte[] data) implements ProtocolRecord { public static Feature deserialize(VLQInputStream in) throws IOException { - return new Feature(in.readUnsignedByte(), in.readNBytes(in.readUnsignedShort())); + return new Feature(in.readUnsignedByte(), in.readNFully(in.readUnsignedShort())); } @Override diff --git a/src/main/java/com/satergo/ergonnection/records/Header.java b/src/main/java/com/satergo/ergonnection/records/Header.java index fc03c26..b0844a8 100644 --- a/src/main/java/com/satergo/ergonnection/records/Header.java +++ b/src/main/java/com/satergo/ergonnection/records/Header.java @@ -9,7 +9,7 @@ public record Header(byte[] bytes) implements ProtocolRecord { public static Header deserialize(VLQInputStream in) throws IOException { - return new Header(in.readNBytes(in.readUnsignedShort())); + return new Header(in.readNFully(in.readUnsignedShort())); } @Override diff --git a/src/main/java/com/satergo/ergonnection/records/Peer.java b/src/main/java/com/satergo/ergonnection/records/Peer.java index 60d6ff6..b21b884 100644 --- a/src/main/java/com/satergo/ergonnection/records/Peer.java +++ b/src/main/java/com/satergo/ergonnection/records/Peer.java @@ -1,6 +1,6 @@ package com.satergo.ergonnection.records; -import com.satergo.ergonnection.StreamUTF8; +import com.satergo.ergonnection.InternalStreamUtils; import com.satergo.ergonnection.VLQInputStream; import com.satergo.ergonnection.VLQOutputStream; import com.satergo.ergonnection.Version; @@ -33,16 +33,16 @@ public boolean hasPublicAddress() { } public static Peer deserialize(VLQInputStream in) throws IOException { - String agentName = StreamUTF8.readByteLen(in); + String agentName = InternalStreamUtils.readUTF8ByteLen(in); Version version = Version.parse(in.readByte() + "." + in.readByte() + "." + in.readByte()); - String peerName = StreamUTF8.readByteLen(in); + String peerName = InternalStreamUtils.readUTF8ByteLen(in); boolean hasPublicAddress = in.readBoolean(); InetSocketAddress publicAddress = null; if (hasPublicAddress) { // Protocol for some reason encodes it as length + 4 int publicAddressLength = in.readUnsignedByte() - 4; publicAddress = new InetSocketAddress( - InetAddress.getByAddress(in.readNBytes(publicAddressLength)), + InetAddress.getByAddress(in.readNFully(publicAddressLength)), // For some reason, it uses u-int instead of u-short (int) in.readUnsignedInt() ); @@ -57,11 +57,11 @@ public static Peer deserialize(VLQInputStream in) throws IOException { @Override public void serialize(VLQOutputStream out) throws IOException { - StreamUTF8.writeByteLen(out, agentName); + InternalStreamUtils.writeUTF8ByteLen(out, agentName); out.write(version().major()); out.write(version().minor()); out.write(version().patch()); - StreamUTF8.writeByteLen(out, peerName); + InternalStreamUtils.writeUTF8ByteLen(out, peerName); out.writeBoolean(hasPublicAddress()); if (hasPublicAddress()) { InetAddress address = publicAddress.getAddress(); diff --git a/src/main/java/com/satergo/ergonnection/records/RawModifier.java b/src/main/java/com/satergo/ergonnection/records/RawModifier.java index dcdcf0f..1049350 100644 --- a/src/main/java/com/satergo/ergonnection/records/RawModifier.java +++ b/src/main/java/com/satergo/ergonnection/records/RawModifier.java @@ -1,5 +1,6 @@ package com.satergo.ergonnection.records; +import com.satergo.ergonnection.InternalStreamUtils; import com.satergo.ergonnection.VLQInputStream; import com.satergo.ergonnection.VLQOutputStream; import com.satergo.ergonnection.protocol.ProtocolRecord; @@ -13,7 +14,7 @@ public record RawModifier(int typeId, byte[] id, byte[] object) implements Proto } public static RawModifier deserialize(int typeId, VLQInputStream in) throws IOException { - return new RawModifier(typeId, in.readNBytes(32), in.readNBytes((int) in.readUnsignedInt())); + return new RawModifier(typeId, in.readNFully(32), in.readNFully((int) in.readUnsignedInt())); } @Override