Nxt Blockchain Tutorial

From Nxtwiki
Jump to: navigation, search

Introduction

Objective

Use a shell script to scan the Nxt blockchain, with an emphasis on understanding blockchain technology rather than speed. Based on the scan() method found in the Nxt software version 1.3.5 source file nxt/src/java/nxt/BlockchainProcessorImpl.Java

Assumptions

Nxt Java software running on your system, and familiarity with bash shell scripts running in a console window on a linux system. You have the jshon, bc and sha256sum utilities installed.

H2 Database Interfaces

The Nxt blockchain is stored in a Java H2 database (DB), located in your Nxt software installation directory (assumed to be nxt) on the path nxt/nxt_db. There are three tools available for interacting with the DB, two of which (DB Shell and H2 Console) use a browser interface and one of which (DB Shell) can operate concurrently with the Nxt Software without changing the nxt/conf/nxt.properties file.

Shell

To interact with the DB from a linux command line, use the Java Shell tool available in nxt/lib. For example

$ cd nxt; java -cp lib/h2*.jar org.h2.tools.Shell \
  -url 'jdbc:h2:./nxt_db/nxt;DB_CLOSE_ON_EXIT=FALSE;CACHE_SIZE=439808;AUTO_SERVER=TRUE' \
  -driver 'org.h2.Driver' -user sa -password sa \
  -sql 'select base_target from block where height=0;'

returns

BASE_TARGET
153722867
(1 row, 19 ms)

The Shell tool will only connect with the DB if the Nxt software is not running, because it locks the DB. To avoid this problem add

nxt.dbUrl=jdbc:h2:nxt_db/nxt;DB_CLOSE_ON_EXIT=FALSE;AUTO_SERVER=TRUE

to the nxt/conf/nxt.properties file and restart the Nxt software. The AUTO_SERVER clause enables DB access from multiple processes simultaneously.

The Shell tool has an interactive mode accessible by invoking it without the -sql option. The command line mode is slow because a DB connection is established every time.

DB Shell

Enter http://localhost:7876/dbshell into a browser. A page loads with a prompt for an SQL statement. dbshell is built into the Nxt software and so must be used while the latter is running. The SQL query

SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='BLOCK';

gives a list of the columns (fields) of the block table, along with information such as data type. This tool is adequate, but the H2 Console is preferred because of its graphical interface.

H2 Console

In your Nxt software installation directory there is a library subdirectory (lib) with a jar file for the H2 DB. If launched as follows

$ cd nxt; java -cp "lib/h2*:lib/*" org.h2.tools.Console

a page opens in your browser with a DB connection dialog. Specify:

 Saved Settings: Generic H2 (Embedded)
 Setting Name: Generic H2 (Embedded)
 Driver Class: org.h2.Driver
 JDBC URL: jdbc:h2:nxt_db/nxt;DB_CLOSE_ON_EXIT=FALSE;AUTO_SERVER=TRUE;CACHE_SIZE=439808
 User Name: sa
 Password: sa

Clicking on Connect will connect with the DB but only if the Nxt software is not running, because it locks the DB. To avoid this problem add

nxt.dbUrl=jdbc:h2:nxt_db/nxt;DB_CLOSE_ON_EXIT=FALSE;AUTO_SERVER=TRUE

to the nxt/conf/nxt.properties file and restart the Nxt software. The AUTO_SERVER clause enables DB access from multiple processes simultaneously.

Use ^C in the shell console to terminate the DB engine, or click on the tool tray icon, then exit.

Database Fields

The BLOCK Table

In the left hand column of the H2 Console all tables in the DB are listed. Expanding the BLOCK table by clicking on "+" shows the block table columns (fields). Clicking the "+" next to each column name shows the column data type:

Field Name Data Type NOT NULL
DB_ID BIGINT(19) yes
ID BIGINT(19) yes
VERSION INTEGER(10) yes
TIMESTAMP INTEGER(10) yes
PREVIOUS_BLOCK_ID BIGINT(19) no
TOTAL_AMOUNT BIGINT(19) yes
TOTAL_FEE BIGINT(19) yes
PAYLOAD_LENGTH INTEGER(10) yes
GENERATOR_PUBLIC_KEY VARBINARY(32) yes
PREVIOUS_BLOCK_HASH VARBINARY(32) no
CUMULATIVE_DIFFICULTY VARBINARY(2147483647) yes
BASE_TARGET BIGINT(19) yes
NEXT_BLOCK_ID BIGINT(19) no
HEIGHT INTEGER(10) yes
GENERATION_SIGNATURE VARBINARY(64) yes
BLOCK_SIGNATURE VARBINARY(64) yes
PAYLOAD_HASH VARBINARY(32) yes
GENERATOR_ID BIGINT(19) yes

These 18 fields define a block on the blockchain in the current version of the Nxt software. Note that the block table has evolved since the genesis block. A complete history of the DB structure can be found in the Nxt software source file nxt/src/java/nxt/NxtDbVersion.java

Note that most fields cannot be NULL. The exceptions are the PREVIOUS_... and NEXT_... fields which link the blocks into a chain both forward and backward. The genesis block has a NULL PREVIOUS_BLOCK_ID and the last (current) block has a NULL NEXT_BLOCK_ID.

Below the column list is a list of indexes. The indexes are all used for sorting various columns for fast retrieval, but the following columns are also restricted to have unique values: DB_ID, HEIGHT, ID, TIMESTAMP. They are all used to uniquely identify blocks. DB_ID is the autoincrement field of the table. It normally increases by one with each new block but gaps can occur in the sequence due to occasional deleted blocks. HEIGHT is zero for the genesis block and increases by one with each block in the blockchain. There are no gaps in this sequence. ID is a unique block ID derived from the hash of some of the block fields. TIMESTAMP is the block creation time measured in number of seconds elapsed since the genesis block.

Note that blocks stored in the BLOCK table are associated with transactions stored in the TRANSACTION table through the fields PAYLOAD_LENGTH and PAYLOAD_HASH, and TOTAL_AMOUNT and TOTAL_FEE. PAYLOAD_LENGTH is the total number of bytes of certain fields of all transactions associated with the block and PAYLOAD_HASH is the hash of all those fields. TOTAL_AMOUNT and TOTAL_FEE are the total amounts and fees of all transactions associated with the block. All four of these block fields are zero when there are no transactions associated with the block.

The TRANSACTION Table

In the left hand column of the H2 Console all tables in the DB are listed. Expanding the TRANSACTION table by clicking on "+" shows the transaction table columns (fields). Clicking the "+" next to each column name shows the column data type:

Field Name Data Type NOT NULL
DB_ID BIGINT(19) yes
ID BIGINT(19) yes
DEADLINE SMALLINT(5) yes
SENDER_PUBLIC_KEY VARBINARY(32) yes
RECIPIENT_ID BIGINT(19) no
AMOUNT BIGINT(19) yes
FEE BIGINT(19) yes
HEIGHT INTEGER(10) yes
BLOCK_ID BIGINT(19) yes
SIGNATURE VARBINARY(64) yes
TIMESTAMP INTEGER(10) yes
TYPE TINYINT(3) yes
SUBTYPE TINYINT(3) yes
SENDER_ID BIGINT(19) yes
BLOCK_TIMESTAMP INTEGER(10) yes
FULL_HASH VARBINARY(32) yes
REFERENCED_TRANSACTION_FULL_HASH VARBINARY(32) no
ATTACHMENT_BYTES VARBINARY(2147483647) no
VERSION TINYINT(3) yes
HAS_MESSAGE BOOLEAN(1) yes
HAS_ENCRYPTED_MESSAGE BOOLEAN(1) yes
HAS_PUBLIC_KEY_ANNOUNCEMENT BOOLEAN(1) yes
EC_BLOCK_HEIGHT INTEGER(10) no
EC_BLOCK_ID BIGINT(19) no
HAS_ENCRYPTTOSELF_MESSAGE BOOLEAN(1) yes

These 25 fields define a transaction the current version of the Nxt software. Note that the transaction table has evolved since the genesis block. A complete history of the DB structure can be found in the Nxt software source file nxt/src/java/nxt/NxtDbVersion.java

Note that most fields cannot be NULL. The exceptions are RECIPIENT_ID, REFERENCED_TRANSACTION_FULL_HASH, ATTACHMENT_BYTES and the EC_BLOCK... fields. A transaction is valid without any of these fields specified.

Below the column list is a list of indexes. The indexes are all used for sorting various columns for fast retrieval, but the following columns are also restricted to have unique values: DB_ID, ID, FULL_HASH.

Note that transactions stored in the TRANSACTION table are associated with blocks stored in the BLOCK table through the fields HEIGHT, BLOCK_ID and BLOCK_TIMESTAMP.

The Nxt API

We could interact directly with the DB as shown above, but for the purpose of this tutorial, our script will obtain block and transaction data using Nxt software API calls.

The getBlock Call

The getBlock API call can be made from the shell using curl while the Nxt software is running:

$ h=300000; curl -s "http://localhost:7876/nxt?requestType=getBlock&height=$h"

This returns a JSON object for block height 300000. There are no extra spaces or newlines in the object, and the object does not include fields with null values. For clarity the object can be formatted as:

{
 "previousBlockHash": "f5006dc3ae97f52aa27dc7c51888db5c6b0553aeeb51ad4bc74350edbef6e84e",
 "payloadLength": 370,
 "totalAmountNQT": "0",
 "generationSignature": "5d08857979a8a900da73d37dc7b8c9d15acbf2644755523dcc3dccae722185ae",
 "generator": "15893842156456397710",
 "generatorPublicKey": "85d34459911c4c5eff7da30966334388d900fefe1082fe7b4d2cbe18706df422",
 "baseTarget": "769068704",
 "payloadHash": "0fcff1f8c4b384ee8e8940328827dd99d5d2aa88f401328195e55a5d5afba226",
 "generatorRS": "NXT-ELWG-9FPV-KTP6-F7L6T",
 "nextBlock": "3635605354783461399",
 "requestProcessingTime": 1,
 "numberOfTransactions": 2,
 "blockSignature": "11a916927177d737ce8869ad892403b61c7b8008d63690ce9ff92250c982630a98d8d9f7b52717af585a97b7475d26d14cd108e6f0704cc5a6cbaef52d21b129",
 "transactions": [
  "5119943785891977024",
  "5340614876563792980"
 ],
 "version": 3,
 "totalFeeNQT": "200000000",
 "previousBlock": "3095547095745888501",
 "cumulativeDifficulty": "11255618847538250",
 "block": "16287921892019728450",
 "height": 300000,
 "timestamp": 32363409
}

The getTransaction Call

The getTransaction API call can be made from the shell using curl while the Nxt software is running:

$ tx=5119943785891977024; curl -s "http://localhost:7876/nxt?requestType=getTransaction&transaction=$tx"

This returns a JSON object for one of the two transactions at block height 300000. There are no extra spaces or newlines in the object, and the object does not include fields with null values. For clarity the object can be formatted as:

{
 "senderPublicKey": "2fc5b938a98829d3cbefa60472eac11fd2cc09e6173b370ad6cc0ede666c6475",
 "signature": "ae92f26b1ded2fd0f154e20cedf9b949ea0b1a1d637bf8e4ebaa52f6dcce7008c6373d2c85b84ea2f919fdd66a6d495005fa3e6b52c89330f7064fabf91ea870",
 "feeNQT": "100000000",
 "requestProcessingTime": 0,
 "type": 2,
 "confirmations": 8217,
 "fullHash": "40570f24bfb10d47692bdfa25f3e175768b713b3ccf4bc4cb9cd7cf2ca094bca",
 "version": 1,
 "ecBlockId": "9591128410195604705",
 "signatureHash": "4c9203be1378e5f8ca940794b180e343ec19ac9d38b7fe2f3c2ad9c792e6c305",
 "attachment": {
  "version.AskOrderCancellation": 1,
  "order": "2574190861734682725"
 },
 "senderRS": "NXT-ADH7-TCCK-R46P-AHZSR",
 "subtype": 4,
 "amountNQT": "0",
 "sender": "10080164044348861925",
 "ecBlockHeight": 299990,
 "block": "16287921892019728450",
 "blockTimestamp": 32363409,
 "deadline": 1440,
 "transaction": "5119943785891977024",
 "timestamp": 32363262,
 "height": 300000
}

The getTransactionBytes Call

The getTransactionBytes API call can be made from the shell using curl while the Nxt software is running:

$ tx=5119943785891977024; curl -s "http://localhost:7876/nxt?requestType=getTransactionBytes&transaction=$tx"

This returns the signed and unsigned byte arrays of the transaction fields above packed in a certain order according to a complex algorithm that has changed since the genesis block:

{
 "unsignedTransactionBytes": "0214fed2ed01a0052fc5b938a98829d3cbefa60472eac11fd2cc09e6173b...",
 "requestProcessingTime": 0,
 "confirmations": 12154,
 "transactionBytes": "0214fed2ed01a0052fc5b938a98829d3cbefa60472eac11fd2cc09e6173b370ad6cc..."
}

Comparison of Database and API fields

Block

Note that there is not an exact correspondence between the 18 BLOCK table fields and the 21 getBlock fields:

DB fields API fields
DB_ID
ID block
VERSION version
TIMESTAMP timestamp
PREVIOUS_BLOCK_ID previousBlock
TOTAL_AMOUNT totalAmountNQT
TOTAL_FEE totalFeeNQT
PAYLOAD_LENGTH payloadLength
GENERATOR_PUBLIC_KEY generatorPublicKey
PREVIOUS_BLOCK_HASH previousBlockHash
CUMULATIVE_DIFFICULTY cumulativeDifficulty
BASE_TARGET baseTarget
NEXT_BLOCK_ID nextBlock
HEIGHT height
GENERATION_SIGNATURE generationSignature
BLOCK_SIGNATURE blockSignature
PAYLOAD_HASH payloadHash
GENERATOR_ID generator
generatorRS
numberOfTransactions
transactions
requestProcessingTime

The missing DB field is DB_ID which is not needed to uniquely identify a block. The extra API fields are the Reed-Solomon version of the generator ID, from which it can be computed, and transaction number and IDs associated with the block, and finally the API request processing time.

Transaction

Note that there is not an exact correspondence between the 25 TRANSACTION table fields and the 25 getBlock fields:

DB fields API fields
DB_ID
ID transaction
DEADLINE deadline
SENDER_PUBLIC_KEY senderPublicKey
RECIPIENT_ID recipient
recipientRS
AMOUNT amountNQT
FEE feeNQT
HEIGHT height
BLOCK_ID block
SIGNATURE signature
signatureHash
TIMESTAMP timestamp
TYPE type
SUBTYPE subtype
SENDER_ID sender
senderRS
BLOCK_TIMESTAMP blockTimestamp
FULL_HASH fullHash
REFERENCED_TRANSACTION_FULL_HASH referencedTransactionFullHash
ATTACHMENT_BYTES attachment object
VERSION version
HAS_MESSAGE
HAS_ENCRYPTED_MESSAGE
HAS_PUBLIC_KEY_ANNOUNCEMENT
EC_BLOCK_HEIGHT ecBlockHeight
EC_BLOCK_ID ecBlockId
HAS_ENCRYPTTOSELF_MESSAGE
confirmations
requestProcessingTime

The missing DB fields are DB_ID, which is not needed to uniquely identify a transaction, and the HAS_... booleans which all refer to messages which might be contained in the attachment and which can be determined by examining the attachment. The API has the extra fields signatureHash, which can be obtained from signature, ...RS; the Reed-Solomon versions of IDs, which can be obtained from the IDs; confirmations, which can be computed by comparing height with the last block height on the blockchain; and requestProcessingTime, which is the time to process the API call.

A Shell Script to Scan the Blockchain

Basic Script

Save the following script to a file named block.sh:

#!/bin/bash

function getFields {
 local fields=$(echo $json | jshon -k)
 local field; local type; local name; local names
 for field in $fields; do
   name="$1_$field"; names="$names $name"
   type=$(echo $json | jshon -e $field -t)
   if [[ $type == "string" || $type == "number" ]]; then
    declare -g $name="$(echo $json | jshon -e $field -u)"
   else
    declare -g $name="$(echo $json | jshon -e $field)"
   fi
   #echo $name=${!name} # dump fields
 done
 declare -g "$1_fields"="$names"
}

err=; synched=0
if ((${#1})); then h=$1; else h=0; fi

while ((!synched)) || ((${#b})); do

 json=$(curl -s "http://localhost:7876/nxt?requestType=getBlock&height=$h")
 if ((${#json} == 0)); then err="server not responding"; break; fi
 getFields 'bk'

 if ((${#bk_errorDescription})); then err="$bk_errorDescription"; break; fi
 echo "height=$bk_height, timestamp=$bk_timestamp"

 # do checks here that only refer to the current block

 if ((h != bk_height)); then err="blocks not in sequential height order"; break; fi
  
 if ((synched)); then # do checks here that refer to the previous block

  if ((b != bk_block)); then err="block ID mismatch"; break; fi
  if ((pB != bk_previousBlock)); then err="previousBlock ID mismatch"; break; fi

 fi

 synched=1; ((++h))
 pB=$bk_block; b=$bk_nextBlock
 for var in $bk_fields; do unset $var; done # clear all block fields
 
done

if ((${#err})); then echo "Error: $err"; fi

Make the file executable:

$ chmod +x block.sh

Run it starting from the genesis block:

$ ./block.sh

The height and timestamp of each block beginning with the genesis block is printed on the console:

height=0, timestamp=0
height=1, timestamp=793
height=2, timestamp=1064
height=3, timestamp=1148
...

This simple version of block.sh will scan the entire blockchain in about two hours.

If the "dump fields" line is uncommented

echo $var=${!var} # dump fields

the field variable names and values will be printed for each block.

Now, some explanatory comments about the script.

Call the API

The linux utility curl is used to get a block from the blockchain using the API call getBlock. It returns a JSON-formatted string.

json=$(curl -s "http://localhost:7876/nxt?requestType=getBlock&height=$h")

where $h is the block height and $json is the JSON-formatted block object retrieved.

Loop Control and Command Line Parameter

The variable $h is used to specify block height. It is initialized to the first command line argument $1 (zero default) and incremented near the end of the main block loop, to be checked against the DB field $bk_height in the next loop iteration. To scan the blockchain beginning at height 300000, use:

$ ./block.sh 300000

The block loop terminates when the end of the blockchain is reached. The last block in the chain will have nextBlock null and therefore nextBlock will be missing from the $json string. The value of $bk_nextBlock is stored in $b near the end of the block loop so that when its length is tested at the beginning of the next loop, it will have zero length ${#b} and terminate the loop, since all field variables are cleared at the end of the loop in case one becomes null in the next iteration.

Parsing the JSON Block Object

The getFields function extracts block and transaction fields from the $json string into variables such as $bk_nextBlock and $tx_version. It uses the linux utility program jshon. Lists of variable names $bk_fields and $tx_fields is kept so that the variables can be unset at the end of each loop, in case there are null DB fields omitted from $json in the next iteration.

Handling Errors

In the event of an improper API call, the $json string will contain an errorDescription field instead of block fields. If this is detected, the block loop breaks and an error report is printed.

All errors are handled the same way, by storing an error message in $err, exiting the loop, and finally printing the message.

Blockchain Integrity Checks

If valid block fields are retrieved, integrity checks can be performed in the main body of the block loop. There are two sections for checks. The first is for checks that only refer to the current block, while the second is for checks that refer to the previous block. The synched variable is used to ensure that the second section is only executed after one block has been processed and first-block variables have been stored near the end of the loop for use in the next block.

For example, $bk_block (the block ID of the current block) is stored as pB=$bk_block near the end of the loop, to be compared with $bk_previousBlock (the block ID of the previous block) in the next loop iteration. This is a simple check of blockchain integrity. The field $bk_previousBlock links the current block to the previous block. Likewise, the field $bk_nextBlock (the block ID of the next block) is stored as the variable $b to be compared with $bk_block (the block ID of the current block) in the next loop iteration.

Additional Blockchain Integrity Checks

The basic shell script can be enhanced to perform additional integrity checks on the blockchain. Each new check slows the program down, sometimes significantly, especially when an external, computationally intensive utility is used. For each check below, add it to the appropriate section. If it belongs in the synched section, store the relevant variables near the end of the block loop before the DB fields are cleared for the next iteration.

Transaction Checks

Transaction Number Check
txNum=$(echo $bk_transactions | jshon -l)
if ((txNum != bk_numberOfTransactions)); then err="numberOfTransactions mismatch"; break; fi
Transaction Loop
txs=$(echo $bk_transactions | jshon -a -u) # get list of transaction IDs
for tx in $txs; do
 json=$(curl -s "http://localhost:7876/nxt?requestType=getTransaction&transaction=$tx")
 if ((${#json} == 0)); then err="server not responding"; break; fi
 getFields 'tx'
 if ((${#tx_errorDescription})); then err="$tx_errorDescription"; break; fi

 json=$(curl -s "http://localhost:7876/nxt?requestType=getTransactionBytes&transaction=$tx")
 if ((${#json} == 0)); then err="server not responding"; break; fi
 getFields 'txb'
 if ((${#txb_errorDescription})); then err="$txb_errorDescription"; break; fi

 # place transaction checks here

 for var in $tx_fields; do unset $var; done # clear all transaction fields
 for var in $txb_fields; do unset $var; done # clear all transaction bytes fields
done
if ((${#err})); then break; fi

The getTransaction API call is used to get the transaction fields given the transaction ID. The getTransactionBytes API call is used to get the signed and unsigned transaction byte array (in hex form). The alternative to the latter is to pack together the transaction fields as done below with block fields, except that the packing algorithm is more complex.

The following transaction checks should be placed within the transaction loop as indicated except for designated pre-loop initializations and post-loop checks.

Amount and Fee Checks

Initialize before the transaction loop:

amt=0; fee=0

Accumulate within the loop:

((amt += tx_amountNQT)); ((fee += tx_feeNQT))

Check after the loop:

if ((amt != bk_totalAmountNQT)); then err="tx amountNQT mismatch"; break; fi
if ((fee != bk_totalFeeNQT)); then err="tx feeNQT mismatch"; break; fi
Payload Length and Hash Checks

Initialize before the transaction loop:

payLen=0; payHex=

Accumulate within the loop:

((payLen += ${#txb_transactionBytes})); payHex=$payHex$txb_transactionBytes

Check after the loop:

if ((payLen / 2 != bk_payloadLength)); then err="payload length mismatch"; break; fi
if [[ $(hexToHash $payHex) != $bk_payloadHash ]]; then err="payload hash mismatch"; break; fi
Check Other Transaction/Block Linkages
if [[ $tx_block != $bk_block ]]; then err="tx/bk block ID mismatch"; break; fi
if ((tx_blockTimestamp != bk_timestamp)); then err="tx/bk timestamp mismatch"; break; fi
if ((tx_height != bk_height)); then err="tx/bk height mismatch"; break; fi
Transaction Version Check
if ((tx_height <= 213000)); then v=0; else v=1; fi
if ((v != tx_version)); then err="tx version mismatch"; break; fi
Transaction Hash and ID Checks
sH=$(hexToHash $tx_signature)
if [[ $sH != $tx_signatureHash ]]; then err="tx signature hash mismatch"; break; fi

if ((bk_version < 3)); then
 fH=$(hexToHash $txb_transactionBytes)
else
 fH=$(hexToHash $txb_unsignedTransactionBytes$tx_signatureHash)
fi
txID=$(hashToID $fH)

if [[ $fH != $tx_fullHash ]]; then err="tx full hash mismatch"; break; fi
if [[ $txID != $tx_transaction ]]; then err="tx ID mismatch"; break; fi
Transaction Signature Check
if ((bk_height > 0)); then
 v=${tx_signature:0:64}; h1=${tx_signature:64}
 Y=$(java Curve25519 $v $h1 $tx_senderPublicKey $bk_version)
 if (($?)); then err="tx signature invalid"; break; fi
 m=$(hexToHash $txb_unsignedTransactionBytes); h2=$(hexToHash $m$Y)
 if [[ $h1 != $h2 ]]; then err="tx signature not verified"; break; fi
fi

This check makes use of the Curve25519 Utility to verify the signature. The genesis block is skipped to avoid five bad signatures.

Block Version Check

if ((bk_height == 0)); then v=-1
elif ((bk_height <= 30000)); then v=1
elif ((bk_height <= 132000)); then v=2
else v=3; fi
if ((v != bk_version)); then err="incorrect version"; break; fi

previousBlock Hash / ID Check

if ((bk_version > 1)) && (($(hashToID $bk_previousBlockHash) != bk_previousBlock)); then
 err="previousBlockHash doesn't give previousBlock ID"; break
fi

This check makes use of the function

function hashToID { # convert 32-byte hex hash to decimal id
 echo $(hexToDec $(hexToLE8 $1))
}

which in turn makes use of the functions

function hexToLE8 { # convert hex string to 8-byte (maximum) little endian
 echo $1 | sed 's/../&\n/g' | head -8 | tac | tr -d '\n'
}
function hexToDec { # convert hex string (any length) to decimal
 echo "ibase=16; ${1^^}" | bc
}

These functions can be included near the top of the script. They are written to echo a result which can be captured with $(...). $1 is the first function argument, $2 the second.

hexToLE8 converts a hex string to an 8-byte (16 hex digit) maximum little endian (LE) string, meaning that the byte order is reversed. All ID's in Nxt are 8-byte maximum LE unsigned integers, although internally in the Nxt software they are stored as signed integers since that is the only option in Java. Thus only the eight most significant bytes of the 32-byte previousBlockHash are used to compute the previousBlock ID.

hexToDec converts arbitrary size hex strings to decimal numbers. The utility bc (basic calculator) requries the hex sting to be capitalized (accomplished by ${1^^}) but can handle large strings, in contrast to the built-in printf %d 0x$1 which can only handle up to 63 bits.

Block ID Check

blockHex=$(decToLE8 4 $bk_version)$(decToLE8 4 $bk_timestamp)
blockHex=$blockHex$(decToLE8 8 $bk_previousBlock)$(decToLE8 4 $bk_numberOfTransactions)
tA=$bk_totalAmountNQT; tF=$bk_totalFeeNQT; w=8
if ((bk_version < 3)); then ((tA /= 100000000)); ((tF /= 100000000)); w=4; fi
blockHex=$blockHex$(decToLE8 $w $tA)$(decToLE8 $w $tF)
blockHex=$blockHex$(decToLE8 4 $bk_payloadLength)$bk_payloadHash$bk_generatorPublicKey$bk_generationSignature
if ((bk_version > 1)); then blockHex=$blockHex$bk_previousBlockHash; fi 
blockHexNoSignature=$blockHex
blockHex=$blockHex$bk_blockSignature
blockHash=$(hexToHash $blockHex)
blockID=$(hashToID $blockHash)
if ((blockID != bk_block)); then err="computed block ID mismatch"; break; fi

$blockID is computed from the hash of a byte string composed of certain block fields packed in a certain order. Decimal number fields must first be converted to hex strings with the function decToLE8, which in turn uses the function hexToLE8 previously introduced:

function decToLE8 { # convert $1-byte (8-byte maximum) decimal $2 to little-endian hex
 local hex=$(printf %0$(($1 * 2))x $2)
 local len=$(($1 * 2 - ${#hex})); if ((len < 0)); then hex=${hex:0-len}; fi # handles version = -1
 echo $(hexToLE8 $hex)
}

Once the hex string blockHex is ready, it is hashed using the function hexToHash, which relies on the utility sha256sum:

function hexToHash { # convert hex string to 32-byte hex hash
 local hash=($(echo -ne $(echo $1 | sed 's/../\\x&/g') | sha256sum))
 echo ${hash[0]}
}

The hex string is prepared by converting it to a byte string as expected by sha256sum; echo must not append a newline nor treat backslashes as escapes, hence echo -ne.

Finally, the block ID is computed from the hash using the previously mentioned hashToID function. It is then checked against the corresponding block field.

Note that the $blockHex string definition depends on the version of the blockchain. Version 1 introduced the previousBlock field, and version 3 replaced NXT with NQT=10-8 NXT in the amount and fee fields.

Block Signature Check

v=${bk_blockSignature:0:64}; h1=${bk_blockSignature:64}
Y=$(java Curve25519 $v $h1 $bk_generatorPublicKey $bk_version)
if (($?)); then err="block signature invalid"; break; fi
m=$(hexToHash $blockHexNoSignature); h2=$(hexToHash $m$Y)
if [[ $h1 != $h2 ]]; then err="block signature not verified"; break; fi

There is no linux utility available for verifying Curve25519 signatures, so the same Curve25519 Java class used by the Nxt software is used. The $bk_blockSignature field is split in half and each half is passed along with $bk_generatorPublicKey to the customized Curve25519 utility, which returns $Y. $Y is hashed with the hash of $blockHexNoSignature, which is just $blockHex without the trailing $bk_blockSignature field.

Curve25519 Utility - Java Version

Curve25519.java can be acquired from the Nxt software directory nxt/src/java/nxt/crypto/Curve25519.java. A main method can be added to the Curve25519 class as follows, after removing the first line (package nxt.crypto;):

final class Curve25519 {

 // Usage: java Curve25519 v h publicKey version
 // signature is (v 32 bytes + h 32 bytes), public key is 32 bytes, version is integer

 public static void main (String[] args) {
  if (args.length != 4) System.exit(1);
  byte [] v = hexStringToByteArray(args[0]);
  byte [] h = hexStringToByteArray(args[1]);
  byte [] publicKey = hexStringToByteArray(args[2]); 
  int version = Integer.parseInt(args[3]);
  if (version >= 3) {
   byte[] signature = new byte[v.length + h.length];
   System.arraycopy(v, 0, signature, 0, v.length);
   System.arraycopy(h, 0, signature, v.length, h.length);
   if(!Curve25519.isCanonicalSignature(signature)) System.exit(2);
   if(!Curve25519.isCanonicalPublicKey(publicKey)) System.exit(3);
  }
  byte[] Y = new byte[32];
  Curve25519.verify(Y, v, h, publicKey);
  System.out.println(bytesToHex(Y));
  System.exit(0);
 }

 public static byte[] hexStringToByteArray(String s) {
  int len = s.length();
  byte[] data = new byte[len / 2];
  for (int i = 0; i < len; i += 2) {
   data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16));
  }
  return data;
 }

 final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
 public static String bytesToHex(byte[] bytes) {
  char[] hexChars = new char[bytes.length * 2];
  for ( int j = 0; j < bytes.length; j++ ) {
   int v = bytes[j] & 0xFF;
   hexChars[j * 2] = hexArray[v >>> 4];
   hexChars[j * 2 + 1] = hexArray[v & 0x0F];
  }
  return new String(hexChars);
 }
  ...
}

This method provides a command line interface that accepts and returns hex strings. It also checks the input fields for canonical form when version >= 3.

Compile the modified class with:

$ javac Curve25519.java

The resulting Curve25519.class and Curve25519$long10.class files are now ready for use with the java command

$ java Curve25519 v h publicKey version
curve25519 Utility - C Version

Curve25519.java was ported to C and is available here. Acquire curve25519.c and compile it with:

$ gcc curve25519.c -o curve25519

This version includes all of the cryptographic methods from the original Java source code, plus a main function that provides a convenient interface allowing key pair generation, signing, verifying and key agreement. Here we only need verification. Everywhere where the Java utility is invoked:

Y=$(java Curve25519 $v $h1 ...PublicKey $bk_version)

the C utility can be invoked instead:

Y=$(./curve25519 -v $v $h1 ...PublicKey)

where the executable is assumed to be in the same directory as block.sh. Note that the C version always checks for canonical form, regardless of version; hence the final argument is omitted.

previousBlockHash Check (Synched)

if ((bk_version > 1)) && [[ $pBH != $bk_previousBlockHash ]]; then err="previousBlockHash mismatch"; break; fi

Insert the following near the end of the main block loop:

pBH=$blockHash

generationSignature Check (Synched)

if ((bk_version == 1)); then
 v=${bk_generationSignature:0:64}; h1=${bk_generationSignature:64}
 Y=$(java Curve25519 $v $h1 $bk_generatorPublicKey $bk_version)
 if (($? > 1)); then err="canonical failure"; break; fi
 m=$(hexToHash $pGS);  h2=$(hexToHash $m$Y)
 if [[ $h1 != $h2 ]]; then err="version == 1 generation signature not verified"; break; fi
else
 generationSignatureHash=$(hexToHash $pGS$bk_generatorPublicKey)
 if [[ $generationSignatureHash != $bk_generationSignature ]]; then
  err="version > 1 generation signature not verified"; break
 fi
fi

Insert the following near the end of the main block loop:

pGS=$bk_generationSignature

When version = 1, $bk_generationSignature is checked exactly like the $bk_blockSignature. With version 2, a simple hash algorithm not involving a signature is used. The change was made because there was a flaw in the algorithm which used generationSignature in forging calculations due to randomness in Curve22519 signature generation.

baseTarget Check (Synched)

bT=$(((pBT * (bk_timestamp - pT)) / 60)); if ((bT > MAX_BASE_TARGET)); then bT=$MAX_BASE_TARGET;fi
if ((bT < pBT / 2)); then ((bT = pBT / 2)); fi
if ((bT == 0)); then bT=1; fi
pBTx2=$((pBT * 2)); if ((pBTx2 > MAX_BASE_TARGET)); then pBTx2=$MAX_BASE_TARGET; fi
if ((bT > pBTx2)); then bT=$pBTx2; fi
if ((bT != bk_baseTarget)); then err="baseTarget mismatch"; break; fi

Insert the following near the end of the main block loop:

pBT=$bk_baseTarget; pT=$bk_timestamp

Insert the following constant initializations before the start of the main block loop:

TWO64=$(echo "ibase=16; 10000000000000000" | bc) # 2^64 in decimal
MAX_BALANCE_NXT=1000000000
GENESIS_BASE_TARGET=$(echo "$TWO64 / (2 * 60 * $MAX_BALANCE_NXT)" | bc) # 153722867
MAX_BASE_TARGET=$((MAX_BALANCE_NXT * GENESIS_BASE_TARGET))

$MAX_BALANCE_NXT is the total amount of NXT in existence, 1 billion. $GENESIS_BASE_TARGET is the initial $bk_baseTarget at height zero. It is used to determine which Nxt node will forge a new block. A hit value is computed for each node based on an effectively random 8-byte (64-bit) number computed from $bk_generationSignature. All nodes with a hit value less than $bk_baseTarget * $tx_effectiveBalance (computed from transaction data) * (time since last block, in seconds) are permitted to submit new blocks to the Nxt network for inclusion in the blockchain. The candidate blockchain with the greatest $bk_cumulativeDifficulty (inversely related to $bk_baseTarget) is accepted by the network. Eventually some node will get a hit because it becomes easier with each passing second.

If all $MAX_BALANCE_NXT NXT in existence were concentrated in one node, $bk_baseTarget * $tx_effectiveBalance would be TWO64 / (2 * 60) which that node's hit value would have a 50% chance of exceeding in 60 attempts. Theoretically then, a hit occurs on average once per minute throughout the network if all NXT tokens are available for forging. In practice the average elapsed time between blocks is somewhat longer.

$bk_baseTarget is dynamically adjusted to maintain a steady blockchain growth rate. If blocks are being generated too slowly, $bk_baseTarget is increased proportional to the time elapsed since the previous block, but is not allowed to more than double nor less than halve with each new block.

cumulativeDifficulty Check (Synched)

cD=$(echo "$pCD + $TWO64 / $bk_baseTarget" | bc)
if [[ $cD != $bk_cumulativeDifficulty ]]; then err="cumulativeDifficulty mismatch"; break; fi

Insert the following near the end of the main block loop:

pCD=$bk_cumulativeDifficulty

The utility bc is necessary, since $TWO64 is out of the normal long integer range and $bk_cumulativeDifficulty grows without limit; it will eventually go out of range.

Complete Script

#!/bin/bash

function hexToLE8 { # convert hex string to 8-byte (maximum) little endian
 echo $1 | sed 's/../&\n/g' | head -8 | tac | tr -d '\n'
}

function hexToDec { # convert hex string (any length) to decimal
 echo "ibase=16; ${1^^}" | bc
}

function hashToID { # convert 32-byte hex hash to decimal id
 echo $(hexToDec $(hexToLE8 $1))
}

function decToLE8 { # convert $1-byte (8-byte maximum) decimal $2 to little-endian hex
 local hex=$(printf %0$(($1 * 2))x $2)
 local len=$(($1 * 2 - ${#hex})); if ((len < 0)); then hex=${hex:0-len}; fi # handles version = -1
 echo $(hexToLE8 $hex)
}

function hexToHash { # convert hex string to 32-byte hex hash
 local hash=($(echo -ne $(echo $1 | sed 's/../\\x&/g') | sha256sum))
 echo ${hash[0]}
}

TWO64=$(echo "ibase=16; 10000000000000000" | bc) # 2^64 in decimal
MAX_BALANCE_NXT=1000000000
GENESIS_BASE_TARGET=$(echo "$TWO64 / (2 * 60 * $MAX_BALANCE_NXT)" | bc) # 153722867
MAX_BASE_TARGET=$((MAX_BALANCE_NXT * GENESIS_BASE_TARGET))

function getFields {
 local fields=$(echo $json | jshon -k)
 local field; local type; local name; local names
 for field in $fields; do
   name="$1_$field"; names="$names $name"
   type=$(echo $json | jshon -e $field -t)
   if [[ $type == "string" || $type == "number" ]]; then
    declare -g $name="$(echo $json | jshon -e $field -u)"
   else
    declare -g $name="$(echo $json | jshon -e $field)"
   fi
   #echo $name=${!name} # dump fields
 done
 declare -g "$1_fields"="$names"
}

err=; synched=0
if ((${#1})); then h=$1; else h=0; fi

while ((!synched)) || ((${#b})); do

 json=$(curl -s "http://localhost:7876/nxt?requestType=getBlock&height=$h")
 if ((${#json} == 0)); then err="server not responding"; break; fi
 getFields 'bk'

 if ((${#bk_errorDescription})); then err="$bk_errorDescription"; break; fi
 echo "height=$bk_height, timestamp=$bk_timestamp"

 # do checks here that only refer to the current block

 txNum=$(echo $bk_transactions | jshon -l)
 if ((txNum != bk_numberOfTransactions)); then err="numberOfTransactions mismatch"; break; fi

 amt=0; fee=0
 payLen=0; payHex=
 txs=$(echo $bk_transactions | jshon -a -u) # get list of transaction IDs
 for tx in $txs; do
  json=$(curl -s "http://localhost:7876/nxt?requestType=getTransaction&transaction=$tx")
  if ((${#json} == 0)); then err="server not responding"; break; fi
  getFields 'tx'
  if ((${#tx_errorDescription})); then err="$tx_errorDescription"; break; fi

  json=$(curl -s "http://localhost:7876/nxt?requestType=getTransactionBytes&transaction=$tx")
  if ((${#json} == 0)); then err="server not responding"; break; fi
  getFields 'txb'
  if ((${#txb_errorDescription})); then err="$txb_errorDescription"; break; fi

  # place transaction checks here

  ((amt += tx_amountNQT)); ((fee += tx_feeNQT))
  ((payLen += ${#txb_transactionBytes})); payHex=$payHex$txb_transactionBytes

  if [[ $tx_block != $bk_block ]]; then err="tx/bk block ID mismatch"; break; fi
  if ((tx_blockTimestamp != bk_timestamp)); then err="tx/bk timestamp mismatch"; break; fi
  if ((tx_height != bk_height)); then err="tx/bk height mismatch"; break; fi

  if ((tx_height <= 213000)); then v=0; else v=1; fi
  if ((v != tx_version)); then err="tx version mismatch"; break; fi

  sH=$(hexToHash $tx_signature)
  if [[ $sH != $tx_signatureHash ]]; then err="tx signature hash mismatch"; break; fi

  if ((bk_version < 3)); then
   fH=$(hexToHash $txb_transactionBytes)
  else
   fH=$(hexToHash $txb_unsignedTransactionBytes$tx_signatureHash)
  fi
  txID=$(hashToID $fH)

  if [[ $fH != $tx_fullHash ]]; then err="tx full hash mismatch"; break; fi
  if [[ $txID != $tx_transaction ]]; then err="tx ID mismatch"; break; fi

  if ((bk_height > 0)); then
   v=${tx_signature:0:64}; h1=${tx_signature:64}
   Y=$(java Curve25519 $v $h1 $tx_senderPublicKey $bk_version)
   if (($?)); then err="tx signature invalid"; break; fi
   m=$(hexToHash $txb_unsignedTransactionBytes); h2=$(hexToHash $m$Y)
   if [[ $h1 != $h2 ]]; then err="tx signature not verified"; break; fi
  fi

  for var in $tx_fields; do unset $var; done # clear all transaction fields
  for var in $txb_fields; do unset $var; done # clear all transaction bytes fields
 done
 if ((${#err})); then break; fi
 if ((amt != bk_totalAmountNQT)); then err="tx amountNQT mismatch"; break; fi
 if ((fee != bk_totalFeeNQT)); then err="tx feeNQT mismatch"; break; fi
 if ((payLen / 2 != bk_payloadLength)); then err="payload length mismatch"; break; fi
 if [[ $(hexToHash $payHex) != $bk_payloadHash ]]; then err="payload hash mismatch"; break; fi

 if ((h != bk_height)); then err="blocks not in sequential height order"; break; fi

 if ((bk_height == 0)); then v=-1
 elif ((bk_height <= 30000)); then v=1
 elif ((bk_height <= 132000)); then v=2
 else v=3; fi
 if ((v != bk_version)); then err="incorrect version"; break; fi  

 if ((bk_version > 1)) && (($(hashToID $bk_previousBlockHash) != bk_previousBlock)); then
  err="previousBlockHash doesn't give previousBlock ID"; break
 fi

 blockHex=$(decToLE8 4 $bk_version)$(decToLE8 4 $bk_timestamp)
 blockHex=$blockHex$(decToLE8 8 $bk_previousBlock)$(decToLE8 4 $bk_numberOfTransactions)
 tA=$bk_totalAmountNQT; tF=$bk_totalFeeNQT; w=8
 if ((bk_version < 3)); then ((tA /= 100000000)); ((tF /= 100000000)); w=4; fi
 blockHex=$blockHex$(decToLE8 $w $tA)$(decToLE8 $w $tF)
 blockHex=$blockHex$(decToLE8 4 $bk_payloadLength)$bk_payloadHash$bk_generatorPublicKey$bk_generationSignature
 if ((bk_version > 1)); then blockHex=$blockHex$bk_previousBlockHash; fi 
 blockHexNoSignature=$blockHex
 blockHex=$blockHex$bk_blockSignature
 blockHash=$(hexToHash $blockHex)
 blockID=$(hashToID $blockHash)
 if ((blockID != bk_block)); then err="computed block ID mismatch"; break; fi

 v=${bk_blockSignature:0:64}; h1=${bk_blockSignature:64}
 Y=$(java Curve25519 $v $h1 $bk_generatorPublicKey $bk_version)
 if (($?)); then err="block signature invalid"; break; fi
 m=$(hexToHash $blockHexNoSignature); h2=$(hexToHash $m$Y)
 if [[ $h1 != $h2 ]]; then err="block signature not verified"; break; fi

 if ((synched)); then # do checks here that refer to the previous block

  if ((b != bk_block)); then err="block ID mismatch"; break; fi
  if ((pB != bk_previousBlock)); then err="previousBlock ID mismatch"; break; fi

  if ((bk_version > 1)) && [[ $pBH != $bk_previousBlockHash ]]; then err="previousBlockHash mismatch"; break; fi

  if ((bk_version == 1)); then
   v=${bk_generationSignature:0:64}; h1=${bk_generationSignature:64}
   Y=$(java Curve25519 $v $h1 $bk_generatorPublicKey $bk_version)
   if (($? > 1)); then err="canonical failure"; break; fi
   m=$(hexToHash $pGS);  h2=$(hexToHash $m$Y)
   if [[ $h1 != $h2 ]]; then err="version == 1 generation signature not verified"; break; fi
  else
   generationSignatureHash=$(hexToHash $pGS$bk_generatorPublicKey)
   if [[ $generationSignatureHash != $bk_generationSignature ]]; then
    err="version > 1 generation signature not verified"; break
   fi
  fi

  bT=$(((pBT * (bk_timestamp - pT)) / 60)); if ((bT > MAX_BASE_TARGET)); then bT=$MAX_BASE_TARGET;fi
  if ((bT < pBT / 2)); then ((bT = pBT / 2)); fi
  if ((bT == 0)); then bT=1; fi
  pBTx2=$((pBT * 2)); if ((pBTx2 > MAX_BASE_TARGET)); then pBTx2=$MAX_BASE_TARGET; fi
  if ((bT > pBTx2)); then bT=$pBTx2; fi
  if ((bT != bk_baseTarget)); then err="baseTarget mismatch"; break; fi

  cD=$(echo "$pCD + $TWO64 / $bk_baseTarget" | bc)
  if [[ $cD != $bk_cumulativeDifficulty ]]; then err="cumulativeDifficulty mismatch"; break; fi

 fi

 synched=1; ((++h))
 pB=$bk_block; b=$bk_nextBlock; pBH=$blockHash; pGS=$bk_generationSignature
 pBT=$bk_baseTarget; pT=$bk_timestamp; pCD=$bk_cumulativeDifficulty
 for var in $bk_fields; do unset $var; done # clear all block fields
 
done

if ((${#err})); then echo "Error: $err"; fi