nostr-java
nostr-java copied to clipboard
NIP-13 Nonce mining
Nonce mining is slow on json object, on string is fast but on bytes is the fastest. Have been trying to develop an nostr server, and looked at the event id mining. Thus gave it a try as a unit test, and thought to share it as you may need equal like code;
@Test
public void testMining() throws Exception {
Duration maxMining = Duration.ofSeconds(360);
int maxDifficulty = (OctoTrust.KEY_LENGTH * 8) / 2; // is 128 thus 50% of bits are zero
NoStrIdentity nid = new NoStrIdentity(NoStrIdentityPrivateKey.ofRandom());
try (JsonReader reader = Json.createReader(new InputStreamReader(getClass().getResourceAsStream("note-text-simple.json")))) {
NoStrEvent event = new NoStrEvent(reader.readObject());
Assertions.assertTrue(NoStrEventSignature.verify(event));
int testOffsetDifficulty = 10; // +12 from note-text-simple.json
// use test public key and copy others
NoStrEventPayload payload = new NoStrEventPayload(nid.getPublicKey(), event.getPayload().getCreatedAt(), event.getPayload().getKind(), event.getPayload().getTags(), event.getPayload().getContent());
Optional<NoStrEventTag> nonceTagOpt = payload.findFirstByQName(NoStrImplEventTag.NONCE);
if (nonceTagOpt.isEmpty()) {
return; // no mining requested
}
// replace X NoStrEventTag impl or NoStrEventTagCustom or NoStrTagNonce with local tag for setter access
NoStrTagNonce nonceTag = new NoStrTagNonce(Integer.parseInt(nonceTagOpt.get().getMetaArgument(NoStrImplTagNonce.DIFFICULTY).get()) + testOffsetDifficulty);
int nonceTagIdx = payload.getTags().indexOf(nonceTagOpt.get());
payload.getTags().set(nonceTagIdx, nonceTag);
int targetDifficulty = nonceTag.getDifficulty();
if (targetDifficulty > maxDifficulty) {
throw new IllegalArgumentException("Target difficulty can't be larger than " + maxDifficulty);
}
int mineCnt = 0;
byte[] eventId = null;
int leadingZeroBits = 0;
byte[] nowStr = Long.toString(payload.getCreatedAt().getEpochSecond()).getBytes(StandardCharsets.UTF_8);
byte[] payloadStr = NoStrEventSignature.generateEventIdJsonString(payload).getBytes(StandardCharsets.UTF_8);
int createdAtLength = nowStr.length;
int createdAtIdx = indexOf(payloadStr, nowStr, indexOf(payloadStr, new byte[]{'\"'}, 5));
int nonceProofIdx = indexOf(payloadStr, "\"nonce\"".getBytes(StandardCharsets.UTF_8), 0) + 9;// ["nonce","7539","12"]],
int nonceProofLength = Integer.toString(nonceTag.getBearerProof()).getBytes(StandardCharsets.UTF_8).length;
long createdAtTime = System.currentTimeMillis();
long maxMiningTime = createdAtTime + maxMining.toMillis();
MessageDigest sha256 = OctoTrustHash.sha256Algorithm();
while (leadingZeroBits < targetDifficulty) {
mineCnt++;
createdAtTime = System.currentTimeMillis();
if (createdAtTime > maxMiningTime) {
throw new IllegalStateException("Mining resource limit exceeded");
}
long createdAtTimeSecs = createdAtTime / 1000;
int createdAtTimeDigits = numDigits(createdAtTimeSecs);
if (createdAtLength != createdAtTimeDigits) {
createdAtLength = createdAtTimeDigits;
payloadStr = growBySplit(payloadStr, createdAtIdx);
}
writeDigits(createdAtTimeSecs, createdAtTimeDigits, payloadStr, createdAtIdx);
int nonceProofDigits = numDigits(mineCnt);
if (nonceProofLength != nonceProofDigits) {
nonceProofLength = nonceProofDigits;
payloadStr = growBySplit(payloadStr, nonceProofIdx);
}
writeDigits(mineCnt, nonceProofDigits, payloadStr, nonceProofIdx);
eventId = sha256.digest(payloadStr);
leadingZeroBits = 0;
for (int i = 0; i < eventId.length; i++) {
int step = eventId[i] & 0xFF; // force unsigned
if (step == 0) {
leadingZeroBits += 8;
continue;
}
leadingZeroBits += Integer.numberOfLeadingZeros(step) - 24;
break;
}
}
payload.setCreatedAt(Instant.ofEpochMilli(createdAtTime));
nonceTag.setBearerProof(mineCnt);
NoStrEvent eventMined = NoStrEventSignature.sign(payload, nid.getPrivateKey(), eventId);
System.out.println("idOrg=" + event.getId().getHex());
System.out.println("idHex=" + eventMined.getId().getHex());
System.out.println("idStr=" + NoStrEventSignature.generateEventIdJsonString(payload));
System.out.println("leadZero=" + leadingZeroBits);
System.out.println("mineCnt=" + mineCnt);
System.out.println("idStrHex=" + OctoBitFormat.HEX.fromBytes(NoStrEventSignature.generateEventIdJsonString(payload).getBytes(StandardCharsets.UTF_8)));
System.out.println("idStrArr=" + OctoBitFormat.HEX.fromBytes(payloadStr));
Assertions.assertTrue(NoStrEventSignature.verify(eventMined));
Assertions.assertEquals("" + mineCnt, eventMined.getPayload().findFirstByQName("nonce").get().getMetaArgument(0).get());
Assertions.assertEquals("" + targetDifficulty, eventMined.getPayload().findFirstByQName("nonce").get().getMetaArgument(1).get());
}
}
public byte[] growBySplit(byte[] src, int splitIdx) {
byte[] result = new byte[src.length + 1];
System.arraycopy(src, 0, result, 0, splitIdx);
System.arraycopy(src, splitIdx + 1, result, splitIdx + 2, src.length - splitIdx - 1);
return result;
}
public void writeDigits(long value, int digits, byte[] target, int off) {
int i = digits - 1;
while (value > 0) {
target[i+off] = (byte) ((value % 10) + '0');
value /= 10;
i--;
}
}
public int numDigits(long n) {
return (int) Math.log10(n) + 1;
}
public int indexOf(byte[] source, byte[] target, int fromIndex) {
return indexOf(source, 0, source.length, target, 0, target.length, fromIndex);
}
public int indexOf(byte[] source, int sourceOffset, int sourceCount, byte[] target, int targetOffset, int targetCount, int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
byte first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
if (source[i] != first) { // search first
while (++i <= max && source[i] != first) {
;
}
}
if (i <= max) { // search left over
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++) {
;
}
if (j == end) {
return i - sourceOffset;
}
}
}
return -1;
}