Index: build/test/test.tcl ================================================================== --- build/test/test.tcl +++ build/test/test.tcl @@ -68,23 +68,36 @@ puts "\[1.FAIL\] Got: $hash" puts "\[1.FAIL\] Exp: $hash_expected" return false } + + # Reduced size test + set data [binary decode hex 4451686437A2BF5C4759100DE2ADE0F39B6877275AF997906B71B1A8EF1550A2] + set hash [binary encode hex [::nano::internal::hashData $data 32]] + set hash_expected "863d40311043ad24e56034de73fb0b77a9f13fbac37ea61368509839ba1832e2" + if {$hash ne $hash_expected} { + puts "\[2.FAIL\] Got: $hash" + puts "\[2.FAIL\] Exp: $hash_expected" + + return false + } return true } proc test_keygeneration {} { - set key [::nano::internal::generateKey] + # Generate a new key pair + set key [::nano::key::generateNewKey] if {[string length $key] != 32} { puts "\[1.FAIL\] Got: [string length $key]" puts "\[1.FAIL\] Exp: 32" return false } + # Generate a public key from the private key set data [binary decode hex 0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF] set pubKey [::nano::internal::publicKey $key] set sig [::nano::internal::signDetached $data $key] set verified [::nano::internal::verifyDetached $data $sig $pubKey] if {!$verified} { @@ -91,19 +104,31 @@ puts "\[2.FAIL\] Got: $verified" puts "\[2.FAIL\] Exp: true" return false } + + # Create a key pair from a seed and index + set seed [binary decode hex C4D214F19E706E9C7487CEF00DE8059200C32414F0ED82E5E33B523AEDF719BA] + set key [::nano::key::computeKey $seed 0] + set pubKey [string toupper [binary encode hex [::nano::internal::publicKey $key]]] + set pubKey_expected "B63EC7A797F2A5858C754EC9C0537920C4F9DEA58F9F411F0C2161F6D303AA7A" + if {$pubKey ne $pubKey_expected} { + puts "\[3.FAIL\] Got: $pubKey" + puts "\[3.FAIL\] Exp: $pubKey_expected" + + return false + } return true } proc test_addressformat {} { set addr nano_35ynhw4qd1pam88azf86nk8ka5sthnzaubcw5fawingep1sjydwaiw8xy7t6 - set pub 8fd47f057582c8998c8fb4c4a48d240f3a7d3e8da55c1b51c851ccb0331f2f88 + set pub 8FD47F057582C8998C8FB4C4A48D240F3A7D3E8DA55C1B51C851CCB0331F2F88 - set pubCheck [string tolower [::nano::address::toPublicKey $addr -hex -verify]] + set pubCheck [string toupper [::nano::address::toPublicKey $addr -hex -verify]] if {$pubCheck ne $pub} { puts "\[1.FAIL\] Got: $pubCheck" puts "\[1.FAIL\] Exp: $pub" return false @@ -117,17 +142,44 @@ return false } return true } + +proc test_blocks {} { + set seed [binary decode hex C4D214F19E706E9C7487CEF00DE8059200C32414F0ED82E5E33B523AEDF719BA] + set key [::nano::key::computeKey $seed 0 -hex] + set address [::nano::address::fromPrivateKey $key -xrb] + + set block [::nano::block::create::receive \ + to $address \ + amount 1000000000000000000000000000000 \ + sourceBlock 207D3043D77B84E892AD4949D147386DE4C2FE4B2C8DC13F9469BC4A764681A7 \ + signKey $key + ] + + set block [::nano::block::create::send \ + from $address \ + to "xrb_1unc5hriitrdjq5dnyhr3zmd8t5hm7rhm9a1u3uun5ycbaacpu649yh5c4b5" \ + previous "D46BFC2E35B5A3CA4230839D67676F4A8498C2567F571D2B66A7F7B72214DEEE" \ + previousBalance 1000000000000000000000000000000 \ + amount 1000000000000000000000000000000 \ + signKey $key + ] + + set block [::nano::block::toDict [::nano::block::fromJSON $block]] + + return true +} set tests { selftest signatures hashing keygeneration addressformat + blocks } foreach test $tests { if {![test_$test]} { puts "FAILED test $test" Index: nano.tcl ================================================================== --- nano.tcl +++ nano.tcl @@ -2,13 +2,15 @@ package require json package require json::write namespace eval ::nano {} +namespace eval ::nano::address {} +namespace eval ::nano::key {} namespace eval ::nano::block {} -namespace eval ::nano::block::account {} -namespace eval ::nano::address {} +namespace eval ::nano::block::create {} +namespace eval ::nano::account {} set ::nano::address::base32alphabet {13456789abcdefghijkmnopqrstuwxyz} proc ::nano::address::toPublicKey {address args} { set performChecksumCheck false set outputFormat "bytes" @@ -135,10 +137,86 @@ set address [string reverse $address] set address "${addressPrefix}${address}" return $address } + +proc ::nano::address::fromPrivateKey {key args} { + set pubKey [::nano::key::publicKeyFromPrivateKey $key] + tailcall ::nano::address::fromPublicKey $pubKey {*}$args +} + +proc ::nano::key::generateNewSeed {} { + tailcall ::nano::internal::generateSeed +} + +proc ::nano::key::generateNewKey {} { + tailcall ::nano::internal::generateKey +} + +proc ::nano::key::computeKey {seed args} { + set index 0 + set outputFormat "bytes" + if {[llength $args] > 0} { + if {[string index [lindex $args 0] 0] ne "-"} { + set index [lindex $args 0] + set args [lrange $args 1 end] + } + + foreach arg $args { + switch -exact -- $arg { + "-hex" { + set outputFormat "hex" + } + "-binary" { + set outputFormat "bytes" + } + default { + return -code error "Invalid option: $arg" + } + } + } + } + if {[string length $seed] != 32} { + set seed [binary decode hex $seed] + } + + set key [::nano::internal::generateKey $seed $index] + if {$outputFormat eq "hex"} { + set key [binary encode hex $key] + } + + return $key +} + +proc ::nano::key::publicKeyFromPrivateKey {key args} { + set outputFormat "bytes" + foreach arg $args { + switch -- $arg { + "-hex" { + set outputFormat "hex" + } + "-binary" { + set outputFormat "bytes" + } + default { + return -code error "Invalid option: $arg" + } + } + } + + if {[string length $key] != 32} { + set key [binary decode hex $key] + } + + set pubKey [::nano::internal::publicKey $key] + if {$outputFormat eq "hex"} { + set pubKey [string toupper [binary encode hex $pubKey]] + } + + return $pubKey +} proc ::nano::block::fromJSON {json} { array set block [json::json2dict $json] switch -- $block(type) { @@ -179,21 +257,63 @@ } return $blockData } -proc ::nano::block::_dictToJSON {blockDict {addArgs_fromPublicKey ""}} { +proc ::nano::block::_dictToJSON {blockDict} { array set block $blockDict - set blockJSONFields {type account source destination previous representative balance link link_as_account _blockHash} + if {[info exists block(signKey)] && ([info exists block(_blockData)] || [info exists block(_blockHash)])} { + if {![info exists block(_blockHash)]} { + set block(_blockHash) [binary encode hex [::nano::block::hash $block(_blockData)]] + } + + set signKey [binary decode hex $block(signKey)] + set blockHash [binary decode hex $block(_blockHash)] + + set signature [::nano::internal::signDetached $blockHash $signKey] + set signature [binary encode hex $signature] + set signature [string toupper $signature] + + set block(signature) $signature + } + + if {$block(type) eq "state"} { + if {![info exists block(link)]} { + set block(link) [::nano::address::toPublicKey $block(link_as_account) -hex] + } + if {![info exists block(link_as_address)]} { + set addressFormatFlag "-nano" + foreach field {account destination representative} { + if {![info exists block($field)]} { + continue + } + if {[string match "nano_*" $block($field)]} { + set addressFormatFlag "-nano" + } else { + set addressFormatFlag "-xrb" + } + + break + } + + set block(link_as_account) [::nano::address::fromPublicKey $block(link) $addressFormatFlag] + } + } + + set blockJSONFields { + type account source destination previous representative balance + link link_as_account _blockHash _workHash signature + } + set blockJSONEntries [lmap field $blockJSONFields { if {![info exists block($field)]} { continue } switch -exact -- $field { - "source" - "previous" - "link" - "_blockHash" { + "source" - "previous" - "link" - "_blockHash" - "_workHash" { set block($field) [string toupper $block($field)] } } return -level 0 [list $field [json::write string $block($field)]] }] @@ -202,26 +322,28 @@ set blockJSON [json::write object {*}$blockJSONEntries] return $blockJSON } -proc ::nano::block::toJSON {blockData args} { +proc ::nano::block::toDict {blockData args} { set block(type) "" set addressPrefix "nano_" foreach arg $args { switch -glob -- $arg { "-type=*" { set block(type) [lindex [split $arg =] 1] } + "-signKey=*" { + set block(signKey) [string range $arg 9 end] + } "-xrb" { set addressPrefix "xrb_" } "-nano" { set addressPrefix "nano_" } default { - return -code error "Invalid option: $arg" } } } if {$block(type) eq ""} { @@ -294,11 +416,19 @@ set block($field) [format %lli "0x$block($field)"] } } } - set blockJSON [_dictToJSON [array get block] ${addArgs_fromPublicKey}] + set block(_blockData) $blockData + + return [array get block] +} + +proc ::nano::block::toJSON {blockData args} { + set blockDict [::nano::block::toDict $blockData {*}$args] + + set blockJSON [_dictToJSON $blockDict] return $blockJSON } proc ::nano::block::hash {blockData args} { @@ -324,78 +454,208 @@ } return $hash } -proc ::nano::block::account::_finalizeBlock {blockDict} { +proc ::nano::block::jsonFromDict {blockDict} { set blockJSON [::nano::block::_dictToJSON $blockDict] set block [::nano::block::fromJSON $blockJSON] - set blockHash [::nano::block::hash $block -hex] - dict set blockDict "_blockHash" $blockHash + set blockHash [::nano::block::hash $block] + + dict set blockDict "_blockHash" [binary encode hex $blockHash] + set blockJSON [::nano::block::_dictToJSON $blockDict] + return $blockJSON } -proc ::nano::block::account::send {args} { +# send from to previousBalance +# amount sourceBlock +# previous ?representative ? +proc ::nano::block::create::send {args} { array set block $args if {![info exists block(representative)]} { set block(representative) $block(from) } - set block(balance) [expr {$block(priorBalance) - $block(amount)}] + set block(balance) [expr {$block(previousBalance) - $block(amount)}] set blockDict [dict create \ "type" state \ "account" $block(from) \ "previous" $block(previous) \ "representative" $block(representative) \ "balance" $block(balance) \ "link_as_account" $block(to) \ + "_workHash" $block(previous) \ ] - tailcall _finalizeBlock $blockDict + if {[info exists block(signKey)]} { + dict set blockDict signKey $block(signKey) + } + + tailcall ::nano::block::jsonFromDict $blockDict } -proc ::nano::block::account::receive {args} { +# Usage: +# receive to previousBalance amount +# sourceBlock ?previous ? +# ?representative ? +proc ::nano::block::create::receive {args} { array set block $args if {![info exists block(representative)]} { set block(representative) $block(to) } if {![info exists block(previous)]} { set block(previous) "0000000000000000000000000000000000000000000000000000000000000000" + set block(previousBalance) 0 + set block(_workHash) [::nano::address::toPublicKey $block(to) -hex] + } else { + set block(_workHash) $block(previous) } - set block(balance) [expr {$block(priorBalance) + $block(amount)}] + set block(balance) [expr {$block(previousBalance) + $block(amount)}] set blockDict [dict create \ "type" state \ "account" $block(to) \ "previous" $block(previous) \ "representative" $block(representative) \ "balance" $block(balance) \ "link" $block(sourceBlock) \ + "_workHash" $block(_workHash) \ ] - tailcall _finalizeBlock $blockDict + if {[info exists block(signKey)]} { + dict set blockDict signKey $block(signKey) + } + + tailcall ::nano::block::jsonFromDict $blockDict } -proc ::nano::block::account::setRepresentative {args} { +# Usage: +# setRepresentative account previous +# representative balance +proc ::nano::block::create::setRepresentative {args} { array set block $args - set block(balance) $block(priorBalance) set block(link) "0000000000000000000000000000000000000000000000000000000000000000" set blockDict [dict create \ "type" state \ "account" $block(account) \ "previous" $block(previous) \ "representative" $block(representative) \ "balance" $block(balance) \ "link" $block(link) \ + "_workHash" $block(previous) \ + ] + + if {[info exists block(signKey)]} { + dict set blockDict signKey $block(signKey) + } + + tailcall ::nano::block::jsonFromDict $blockDict +} + +# -- Tracked accounts -- +proc ::nano::account::setFrontier {account frontierHash balance representative} { + set accountPubKey [::nano::address::toPublicKey $account -hex] + set ::nano::account::frontiers($accountPubKey) [dict create \ + frontierHash $frontierHash balance $balance representative $representative \ + ] +} + +proc ::nano::account::getFrontier {account args} { + set accountPubKey [::nano::address::toPublicKey $account -hex] + if {![info exists ::nano::account::frontiers($accountPubKey)]} { + set frontier [dict create balance 0] + } else { + set frontier $::nano::account::frontiers($accountPubKey) + } + + return [dict get $frontier {*}$args] +} + +proc ::nano::account::addPending {account blockHash amount} { + set accountPubKey [::nano::address::toPublicKey $account -hex] + + set ::nano::account::pending([list $accountPubKey $blockHash]) [dict create amount $amount] +} + +proc ::nano::account::receive {account blockHash signKey} { + set accountPubKey [::nano::address::toPublicKey $account -hex] + + set frontierInfo [getFrontier $account] + dict with frontierInfo {} + + set blockInfo $::nano::account::pending([list $accountPubKey $blockHash]) + unset ::nano::account::pending([list $accountPubKey $blockHash]) + + set amount [dict get $blockInfo amount] + set blockArgs [list to $account previousBalance $balance \ + amount $amount sourceBlock $blockHash \ + signKey $signKey representative $representative] + + if {[info exists frontierHash]} { + lappend blockArgs previous $frontierHash + } + + set block [::nano::block::create::receive {*}$blockArgs] + + set newFrontierHash [dict get [json::json2dict $block] "_blockHash"] + set balance [expr {$balance + $amount}] + + setFrontier $account $newFrontierHash $balance $representative + + return $block +} + +proc ::nano::account::send {fromAccount toAccount amount signKey} { + set fromAccountPubKey [::nano::address::toPublicKey $fromAccount -hex] + set toAccountPubKey [::nano::address::toPublicKey $fromAccount -hex] + + set fromFrontierInfo [getFrontier $fromAccount] + set toFrontierInfo [getFrontier $toAccount] + + set fromBalance [dict get $fromFrontierInfo balance] + set fromFrontierHash [dict get $fromFrontierInfo frontierHash] + set fromRepresentative [dict get $fromFrontierInfo representative] + + set signKey [binary encode hex $signKey] + + set block [::nano::block::create::send \ + from $fromAccount \ + to $toAccount \ + previous $fromFrontierHash \ + previousBalance $fromBalance \ + amount $amount \ + signKey $signKey ] - tailcall _finalizeBlock $blockDict + set newBalance [expr {$fromBalance - $amount}] + set newFrontierHash [dict get [json::json2dict $block] "_blockHash"] + + setFrontier $fromAccount $newFrontierHash $newBalance $fromRepresentative + addPending $toAccount $newFrontierHash $amount + + return $block } +proc ::nano::account::receiveAllPending {key} { + set outBlocks [list] + + set accountPubKey [::nano::key::publicKeyFromPrivateKey $key -hex] + + set signKey [binary encode hex $key] + set account [::nano::address::fromPublicKey $accountPubKey] + + foreach accountPubKeyBlockHash [array names ::nano::account::pending [list $accountPubKey *]] { + set blockHash [lindex $accountPubKeyBlockHash 1] + lappend outBlocks [receive $account $blockHash $signKey] + } + + return $outBlocks +} package provide nano 0