#!/usr/bin/perl

package FCC::node;

#######################################
#                                     #
#     FCC & PTTP Node                 #
#                                     #
#    (C) 2018 Chaosje, Domero         #
#                                     #
#######################################

use strict;
no strict 'refs';
use warnings;
use Exporter;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

$VERSION     = '3.2.6';
@ISA         = qw(Exporter);
@EXPORT      = ();
@EXPORT_OK   = qw();

use POSIX;
use JSON;
use gerr 1.02;
use gfio 1.11;
use Digest::SHA qw(sha256_hex sha512_hex);
use Crypt::Ed25519;
use gserv 4.3.2;
use gclient 8.1.2;
use Time::HiRes qw(gettimeofday usleep);
use FCC::global 2.3.2;
use FCC::wallet 2.1.4;
use FCC::fcc 1.3.6;
use gparse;
use glib;

my $DEBUG = -f "debug$FCCEXT" ? 1 : 0;
my $DEBUGMODE = 0;
my $AVT = 0;

# some to become config
my $SERVER;              # gserv-handle of node-server
my $SERVQUIT=0;          # prevent double quit
my $FIREWALL = {};       # IP firewall
my $CHECKROUTING=1;      # check if we have checked if we have portforwarding
my $PARENTLOOP=0;        # loop all parents one by one
my $SERVERNODE;          # active node in parentloop
my $LEAFLOOP=0;          # loop all leaves one by one
my $SERVERLEAF;          # active leaf in leafloop
my $DATASENT=0;          # bytes sent in total
my $DATARECEIVED=0;      # bytes received in total
my $EVALMODE=0;            # soon to be a goner!

# Infomode is a way to restore from hardforks in combination with the FCC-server (a must in dev-mode)
my $INFOMODE=0;          # Run node to gain information
my $INFOTIME=0;          # Just some extra time to let more nodes connect
my $INFODOWNLOAD=0;      # download the best ledger?
my $INFONODE;            # node to read ledger from
my $INFOLL=0;            # ledgerlength wanted
my $INFOPOS=0;           # position we are downloading ledger from
my $INFOREAD=0;          # event busy?

my $LASTCLIENTRUN=0;     # maintenance timekeeper
my $WALLET;              # node wallet
my $WALLETPASS;          # node wallet password, if protected
my $FCCSERVER = [ $FCCSERVERHOST, $FCCSERVERPORT ];
my $FCCSERVERLAN = [ '192.168.1.103', $FCCSERVERPORT ]; # debug on LAN mode (localmode)
my $FCCHANDLE;           # gclient handle to the FCC-Server
my $CYCLELOOP=0;         # ready to check for votes?
my $FCCINIT=0;           # Initialisation state
my $STATUSTIME=time;     # status-message timekeeper
my $FCCRECONNECT= { sec => 0, time => time };
my $FCCHANDSHAKE=0;      # FCC-server identified?
my $TRYTIME=0;           # Start time of accumulating nodes on init
my $MAXNODES=500;        # max clients to our node
my $MAXPARENTS=500;      # max parents to connect to
my $MAXFAULTS=3;         # max faults a client may make
my $FCCDNS = [
  'http://icanhazip.com/',
  'http://plain-text-ip.com/',
  'https://api.ipify.org/',
  'https://wtfismyip.com/text'
];
my $CORENODES=0;
my $LEDGERLEN=0;
my $LASTBLOCK;
my $PARENTLIST={};
my $NODES={};
my $LEAVES={};
my $CLIENTIP={};

my $SYNC={};
my $SYNCING=0;
my $FIRSTSYNC=1;
my $LEDGERWANTED=0;
my $LEDGERTODO=0;
my $SYNCPOS=0;
my $SYNCBUF={};
my $SYNCSTORED=0;
my $REQUESTED=0;
my $REQBLOCKPOS=0;

my $TRESLIST={};
my $SESSLIST={};
my $TRANSLIST={};
my $TRANSLISTDONE={};
my $TRANSDISTLIST=[];
my $TRANSDIST=0;
my $TRANSDISTDATA;
my $TRANSDISTDONE={};
my $BLOCKLIST={};
my $VOTE={};
my $VOTING=0;
my $ADDCOINBASE;
my $TRANSCATCHUP={};
my $UPDATEFILES=[];
my $UPDATEDIR;
my $UPDATEMODE=1;
my $SHUTDOWNMODE=0;
my $CURVERSION;
my $COINBASELIST=[];
my $MINING=0;
my @SOLMINER=();
my $CALLBACKTIME=0;

# import_ledger file sync.
my $IMPORT_FILE;
my $IMPORT_FH = undef;
my $IMPORT_SIZE = 0;
my $IMPORT_ACTIVE = 0;

my $SYNC_CHECKS = {};  # {pos => [{mask=>, data=>, hash=>, client=>}]}
my $MIN_CONFIRM_NODES = 3;  # Min nodes voor bevestiging
my $LAST_SYNC_POS;  # Laatste Syncing Position

$SIG{'INT'}=\&intquit;
$SIG{'TERM'}=\&termquit;
$SIG{'PIPE'}=\&sockquit;
$SIG{__DIE__}=\&fatal;
$SIG{__WARN__}=\&fatal;

binmode(STDOUT);
binmode(STDERR);

1;

sub infomode {
  $INFOMODE=1; $INFODOWNLOAD=0
}

sub prout {
  my (@txt) = @_;
  my $text=join('',@txt);
  if (!$DEBUGMODE && (substr($text,0,3) eq ' *>')) { return }
  $text =~ s/\n$//;
  my ($s,$m,$h) = localtime(time + $FCCTIME);
  if (length($m)<2) { $m="0$m" }
  if (length($h)<2) { $h="0$h" }
  my $tm="[$h:$m] ";
  my @lines = split(/\n/,$text);
  foreach my $line (@lines) {
    while (length($line)>71) {
      my $sl=substr($line,0,67)." ..";
      my $space=(' 'x(71-length($sl)));
      print STDOUT "\r$tm$sl$space\n";
      $line=substr($line,67)
    }
    my $space=(' 'x(71-length($line)));
    print STDOUT "\r$tm$line$space\n"
  }
}

sub datasent {
  return $DATASENT
}

# OneHippy
sub getci {
  my ($client,$var,$function) = @_;
  if ($client->{fcc}) {
    if ($client->{fcc}{function} eq $function) {
      push @$var,{ host => $client->{mask}, starttime => $client->{fcc}{hellotime}, faults => $client->{fcc}{faults}, version => $client->{fcc}{version} }
    }
  }
}
sub connectinfo {
  my $parents=[];
  foreach my $mask (keys %$PARENTLIST) {
    my $node=$PARENTLIST->{$mask}; my $c=$node->{handle};
    if ($c) {
      push @$parents,{ mask => $mask, starttime => $c->{fcc}{hellotime}, faults => $c->{fcc}{faults}, version => $c->{fcc}{version} }
    }
  }
  my $leaves=[]; my $miners=[]; my $nodes=[]; my $unknowns=[];
  $SERVER->broadcastfunc(\&getci,$leaves,'leaf');
  $SERVER->broadcastfunc(\&getci,$miners,'miner');
  $SERVER->broadcastfunc(\&getci,$nodes,'node');
  $SERVER->broadcastfunc(\&getci,$unknowns,'unknown');
  return { parents => $parents, leaves => $leaves, miners => $miners, nodes => $nodes, unknowns => $unknowns }
}
sub setserv {
 my($ip,$port)=@_;
 $FCCSERVERLAN = [ $ip,$port ];
}

################# FCC MAGIC #############################################################################################################

sub fccconnect {
  my ($localmode) = @_;
  my $txt=" * Connecting to $COIN-SERVER ";
  if ($localmode) {
    prout($txt,join(":",@$FCCSERVERLAN)," .. ");
    $FCCHANDLE=gclient::websocket(@$FCCSERVERLAN,0,\&handlefccserver,0)
  } else {
    prout($txt,join(":",@$FCCSERVER)," .. ");
    $FCCHANDLE=gclient::websocket(@$FCCSERVER,0,\&handlefccserver,1)
  }
  if ($FCCHANDLE->{error}) {
    prout(" * Connecting to $COIN-Server ".join(":",@$FCCSERVER)." Failed! $FCCHANDLE->{error}\n"); return 0
  } else {
    $FCCINIT=3
  }
  $FCCHANDLE->{fcc}={ isparent=>1, isfccserver => 1 };
  $FCCHANDLE->takeloop();
  return 1
}

sub newwall {
  prout("Not found! Create wallet now? (Y/n) ");
  my $res=<STDIN>; chomp $res;
  if (substr(lc($res),0,1) eq 'n') { exit }
  $WALLET=newwallet();
  prout("Encode wallet with password [ leave blank for none ]: ");
  $WALLETPASS=<STDIN>; chomp $WALLETPASS;
  savewallet($WALLET,$WALLETPASS)
}

sub start {
  allowsave();
  if ((defined $_[0]) && (uc($_[0]) eq 'PTTP')) {
    setcoin('PTTP'); shift @_;
    $FCCSERVER = [ $FCCSERVERHOST, $FCCSERVERPORT ];
    $FCCSERVERLAN = [ '192.168.1.103', $FCCSERVERPORT ]; # debug on LAN mode (localmode)
  }
  my ($myport,$slavemode,$localmode,$fccserv) = @_;
  if ($fccserv) {
    if ($localmode) { $FCCSERVERLAN->[0]=$fccserv } else { $FCCSERVER->[0]=$fccserv }
  }
  $IMPORT_FILE = "import_ledger$FCCEXT";
  if (-e "update$FCCEXT") { unlink("update$FCCEXT") }
  my $vers=join('.',substr($FCCVERSION,0,2)>>0,substr($FCCVERSION,2,2));
  # in slavemode call $node->takeloop() while $node->{server}{running}
  # use localmode for testing on LAN
  if ($COIN eq 'PTTP') {
    prout(<<EOT);

  PPPP  TTTTT TTTTT  PPPP
  P   P   T     T    P   P   FULL NODE SERVER v$FCCBUILD
  PPPP    T     T    PPPP      Ledger Version: $vers
  P       T     T    P           (C) 2018 Domero
  P       T     T    P

EOT
  } else {
    prout(<<EOT);

  FFFF  CCC   CCC
  F    C     C          FULL NODE SERVER v$FCCBUILD
  FF   C     C            Ledger Version: $vers
  F    C     C              (C) 2018 Domero
  F     CCC   CCC
  
EOT
  }
  prout("Opening wallet .. ");
  if (!walletexists()) {
    newwall()
  } else {
    if (-e "nodewallet$FCCEXT") { 
      $WALLET=decode_json(gfio::content("nodewallet$FCCEXT")) 
    } else {
      if (walletisencoded()) {
        prout("\nEnter wallet password .. ");
        $WALLETPASS=<STDIN>; chomp $WALLETPASS;
        if (!validwalletpassword($WALLETPASS)) {
          prout("Illegal password!\n"); exit
        }
      }
      my $wlist=loadwallets($WALLETPASS);
      if ($#{$wlist} < 0) {
        newwall()
      } elsif ($#{$wlist} == 0) {
        $WALLET=$wlist->[0]
      } else {
        prout("\nChoose a wallet .. \n");
        my $num=0;
        foreach my $w (@$wlist) {
          $num++; prout("$num\. ");
          if ($w->{name}) { prout($w->{name}."\n   ") }
          prout($w->{wallet}."\n")
        }
        prout("\n0. exit\n\nMake a choice .. ");
        my $ch=<STDIN>; chomp $ch;
        if (!$ch) { exit }
        if ($ch =~ /[^0-9]/) { exit }
        if (($ch < 1) || ($ch > $num)) { exit }
        $WALLET=$wlist->[$ch-1]
      }
      gfio::create("nodewallet$FCCEXT",encode_json({ name => $WALLET->{name}, wallet => $WALLET->{wallet} }))
    }
  }
  prout($WALLET->{name}." ".$WALLET->{wallet});
  prout("Searching our IP .. ");
  my $myip=myip(); if (!$myip) { prout("Failed!\n"); exit }
  my $localip=gclient::localip();
  prout("$myip ($localip)");
  prout("Starting $COIN Node Server $FCCBUILD (ledger $vers)");
  prout("+++ Making time and space worth loving for +++"); prout(' ');
  if (!$myport) {
    if ($COIN eq 'PTTP') {
      $myport=9633
    } else {
      $myport=7050 
    }
  }
  # start node
  $SERVER=gserv::init(\&handleserver,\&serverloop);
  $SERVER->{fcc}={};
  $SERVER->{fcc}{ip}=$myip;
  $SERVER->{fcc}{localip}=$localip;
  $SERVER->{fcc}{port}=$myport;
  $SERVER->{server}{port}=$myport;
  $SERVER->{fcc}{slavemode}=$slavemode;
  $SERVER->{fcc}{localmode}=$localmode || 0;
  $SERVER->{fcc}{status}=0;
  $SERVER->{fcc}{ledgersynced}=0;
  $SERVER->{maxclients}=$MAXNODES;
  $SERVER->{websocketmode}=1;
  $SERVER->{killhttp}=1;
  $SERVER->{pingtime}=80+int(rand(20));
  $SERVER->{debug}=0;
  $SERVER->{name}="$COIN Node $FCCBUILD (ledger $vers) by Chaosje";
  push @{$SERVER->{allowedip}},'*';
  $SERVER->{verbose}=$DEBUG;
  $SERVER->{verbosepingpong}=($DEBUG>1);
  if ($localmode) {
    $SERVER->{fcc}{host}=$localip;
    $SERVER->{fcc}{mask}=$localip.":".$myport
  } else {
    $SERVER->{fcc}{host}=$myip;
    $SERVER->{fcc}{mask}=$myip.":".$myport    
  }
  # now we go into infinity
  $SERVER->{starttime} = time();
  prout(" ** Starting server $SERVER->{name}");
  $SERVER->start(!$slavemode,\&serverloop);
  if ($SERVER->{error}) {
    prout(" ** Could not start server: $SERVER->{error}\n")
  }
  return $SERVER
}

sub killserver {
  my ($msg) = @_;
  if ($SERVQUIT) { exit }
  $SERVQUIT=1;
  if (!$msg) { $msg="Node-Server terminated" }
  prout(" !! Killing server .. $msg");
  savedb();
  my @nodes=keys %$PARENTLIST;
  foreach my $mask (@nodes) {
    my $node=$PARENTLIST->{$mask};
    if ($node->{handle}) {
      if ($node->{identified}) {
        if ($FCCINIT == 255) { $REQUESTED -- }
      }
      delete $PARENTLIST->{$mask};
      $node->{handle}->wsquit($msg);
    }
  }
  if ($FCCHANDLE) {
    $FCCHANDLE->wsquit($msg);
  }
  $SERVER->quit();
  prout(" *!* Server killed *!*");
  print STDOUT "\n";
  exit
}

sub killclient {
  my ($client,$msg) = @_;
  if (!$msg) { $msg='quit' }  
  if (defined $client && (ref($client) eq 'HASH') && !$client->{iskilled}) {
    my $f=""; if ($client->{ip} && $CLIENTIP->{$client->{ip}}) { $CLIENTIP->{$client->{ip}}-- }
    if ($client->{fcc} && $client->{fcc}{function}) {
      $f='('.$client->{fcc}{function}.')'
    }
    prout("   x Disconnected $f $client->{mask}: $msg\n");
    if ($client->{fcc}{isparent}) {
      if ($PARENTLIST->{$client->{mask}}{identified}) {
        if ($FCCINIT == 255) { $REQUESTED -- }
      }
      delete $PARENTLIST->{$client->{mask}};
      $client->wsquit($msg);
      $client->{iskilled}=1;
      $client->takeloop();
    } elsif ($client->{fcc}) {
      if ($client->{fcc}{function} eq 'node') {
        if ($VOTING && $client->{fcc}{ready}) { $VOTE->{totak}-- }
        delete $NODES->{$client->{mask}}
      } elsif ($client->{fcc}{function} eq 'leaf') {
        delete $LEAVES->{$client->{mask}};
        if ($TRESLIST->{$client->{mask}}) {
          delete $TRESLIST->{$client->{mask}}
        }
      } elsif ($client->{fcc}{function} eq 'miner') {
        delete $LEAVES->{$client->{mask}};
      }
      wsmessage($client,$msg,'close');
      $client->{iskilled}=1
    }
  }
}

sub fatal {
    if ($EVALMODE) { return }
    killserver("Fatal Error:".join("\n",error("return=1","!!!! FATAL ERROR !!!!\n",@_,"\n")));
}

sub intquit {
  #print " ---> Servquit = $SERVQUIT           \n";
  if (!$SERVQUIT) {
    killserver('130 Interrupt signal received');
    $SERVQUIT=1
  }
  exit
}  
sub termquit {
  killserver('108 Client forcably killed connection'); exit
}
sub sockquit {
  my $client=$SERVER->{activeclient};
  if ($client) {
    killclient($client,"32 TCP/IP Connection error")
  } elsif ($SERVERNODE) {
    killclient($SERVERNODE,"32 TCP/IP Connection error");
  } else {
    prout(" *!* WARNING *!* Unexpected SIGPIPE in node-kernel. @_\n")
  }
}

################ Global functions ###################

sub myip {
  my $ip=""; my $p=0;
  while (($ip !~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) && ($p<=$#{$FCCDNS})) {
    my $res=website($FCCDNS->[$p]);
    $ip=$res->content(); if ($ip) { chomp($ip) } else { $ip="" }
    $p++;
  }
  return $ip
}

sub takeloop {
  my ($client) = @_;
  if ($SERVER->{slavemode}) {
    $SERVER->takeloop()
  }
}

sub checklanwan {
  my ($host) = @_;
  if (($host =~ /^192.168/) || ($host =~ /^10.0.0/) || ($host =~ /^127.0.0.1/)) {
    return $SERVER->{fcc}{localmode}
  }
  return !$SERVER->{fcc}{localmode}
}

sub outws {
  my ($client,$msg,$command) = @_;
  if ($DEBUG) {
    my $func=$client->{fcc}{function};
    if ($client->{fcc}{isparent}) { $func='parent' }
    prout("SERV >OUT WS ($func): $command - $msg\n")
  }
  if ($client->{fcc}{isparent}) {
    $client->wsout($msg,$command)
  } else {
    wsmessage($client,$msg,$command)
  }
}

sub outjson {
  my ($client,$msg) = @_;
  if ($client && $client->{fcc} && !$client->{killed}) {
    if ($DEBUG) {
      my $func=$client->{fcc}{function};
      if ($client->{fcc}{isfccserver}) { $func='FCC' }
      elsif ($client->{fcc}{isparent}) { $func='parent' }
      prout("SERV >OUT JSON ($func): ")
    }
    if (!$msg) {
      error "FCC::node::outjson: Empty message"
    }
    if (!ref($msg)) { 
      error "Message has to be array or hash reference to be converted to JSON"
    }
    $EVALMODE=1;
    my $json; eval { $json=encode_json($msg) };
    $EVALMODE=0;
    if ($@) { error "JSON error: $@" }
    if ($DEBUG) {
      if (ref($msg) eq 'HASH') {
        my @out=();
        foreach my $k (sort keys %$msg) {
          my $v=$msg->{$k};
          if (ref($v) eq 'HASH') {
            push @out,"$k={".join(", ",sort (keys %$v))."}"
          } elsif (ref($v) eq 'ARRAY') {
            my $num=$#{$v};
            push @out,"$k=ARRAY[$num]"
          } elsif (defined($v)) {
            if (length($v)>500) { $v=substr($v,0,500)." ..." }
            push @out,"$k=$v"
          } else {
            push @out,"$k=[undef]"
          }
        }
        prout(join(", ",@out),"\n")
      } else {
        prout("$json\n")
      }
    }
    if ($client->{fcc}{isparent}) {
      gclient::wsout($client,$json)
    } else {
      wsmessage($client,$json)
    }
  }
}

sub outnc {
  my ($client,$data) = @_;
  if (($client->{fcc}{function} eq 'node') && ($client->{fcc}{ready})) {
    if ($DEBUG) {
      prout("SERV >OUT JSON (node): $data")
    }
    wsmessage($client,$data)
  }
}

sub outparents {
  my ($data) = @_;
  foreach my $p (keys %$PARENTLIST) {
    my $node=$PARENTLIST->{$p};
    if ($node->{identified}) {
      if ($DEBUG) {
        prout("SERV >OUT JSON (parent): $data")
      }
      gclient::wsout($node->{handle},$data)
    }
  }
}

sub outjsonparents {
  my ($data) = @_;
  foreach my $p (keys %$PARENTLIST) {
    my $node=$PARENTLIST->{$p};
    if ($node->{identified}) {
      outjson($node->{handle},$data)
    }
  }
}

sub outcore {
  my ($data) = @_;
  $SERVER->broadcastfunc(\&outnc,$data);
  outparents($data)
}

sub outcorejson {
  my ($data) = @_;
  if (ref($data) ne "HASH") { error("Outcorejson: HASH-ref expected") }
  outcore(encode_json($data))
}

sub getcl {
  my ($client,$nodes) = @_;
  if (($client->{fcc}{function} eq 'node') && ($client->{fcc}{ready})) {
    push @$nodes,$client
  } 
}

sub corelist {
  my $nodes=[];
  $SERVER->broadcastfunc(\&getcl,$nodes);
  foreach my $p (keys %$PARENTLIST) {
    my $node=$PARENTLIST->{$p};
    if ($node->{identified}) {
      push @$nodes,$node->{handle}
    }
  }
  return $nodes
}

sub addparent {
  my ($mask) = @_;
  my $node=$PARENTLIST->{$mask}; my $ctm=gettimeofday();
  $node->{tried}=1; $node->{lastconnect}=$ctm;
  my $newclient=gclient::websocket($node->{host},$node->{port},0,\&handlenode);
  $newclient->{mask}=$mask;
  if (!$newclient->{error}) {
    $newclient->{fcc} = { host=>$node->{host}, port=>$node->{port}, function=>'node', isparent=>1, identified=>0, hellotime=>$ctm };
    $node->{handle}=$newclient;
    #prout(" + Connected to $mask\n")
  } else {
    prout($newclient->{error},"\n");
    if (!$node->{connectcount}) {
      $node->{connectcount}=1
    } else {
      $node->{connectcount}++
    }
    if ($node->{connectcount}>=3) {
      my $mask=$node->{host}.':'.$node->{port};
      prout("Node $mask is unreachable\n");
      if ($PARENTLIST->{$mask}) {
        delete $PARENTLIST->{$mask}
      }
    }
  }
}

sub addcandidate {
  my ($host,$port) = @_;

  # Do not connect to ourself!
  if (($host eq $SERVER->{fcc}{host}) && ($port eq $SERVER->{fcc}{port})) { return }
  #if (!$SERVER->{fcc}{localmode}) {
  #  if (!$INFOMODE) {
  #    if ($host eq $SERVER->{fcc}{ip}) { return }
  #  }
  #}
  my $mask=join(":",$host,$port);
  if (defined $NODES->{$mask}) { return } # already connected as a child
  foreach my $bip (@{$SERVER->{blockedip}}) { if ($bip eq $host) { return } } # blocked by firewall

  if (!$PARENTLIST->{$mask}) {
    $PARENTLIST->{$mask} = {
      host => $host,
      port => $port,
      mask => $mask,
      handle => undef,
      connected => 0,
      identified => 0,
      lastconnect => 0,
      tried => 0,
      ledgerlen => 0,
      lastcum => "",
      blockheight => -1
    }
  }
}

sub minerspresent {
  foreach my $leaf (keys %$LEAVES) {
    if ($leaf->{fcc} && $leaf->{fcc}{function} eq 'miner') { return 1 }
  }
  return 0
}

sub savedb {
  prout(" ** Saving databases .. ");
  allowsave();
  save(); prout(" ** Databases saved OK!");
}

sub addnodelist {
  my ($host,$port) = @_;
  my $sm="$host $port";
  if (!-e "nodelist$FCCEXT") {
    gfio::create("nodelist$FCCEXT",$sm)
  } else {
    my @nodes = split(/\n/,gfio::content("nodelist$FCCEXT"));
    my @out=();
    foreach my $node (@nodes) {
      if ($node eq $sm) { return }
      push @out,$node
    }
    unshift @out,$sm;
    gfio::create("nodelist$FCCEXT",join("\n",@out))
  }
}

sub firewall {
  # todo: make timebased
  my ($ip) = @_;
  if (!defined $FIREWALL->{$ip}) { $FIREWALL->{ip}=1 } 
  else { 
    $FIREWALL->{$ip}++;
    if ($FIREWALL->{$ip} >= 10) {
      prout(" ! blocked IP $ip");
      push @{$SERVER->{blockedip}},$ip
    }
  }
}

################## FCC-Server ################################

sub outfcc {
  my ($data) = @_;
  outjson($FCCHANDLE,$data)
}

sub handlefccserver {
  my ($client,$command,$data) = @_;
  if ($DEBUG) {
    if (!$data) { $data="" }
    prout(" <- [$COIN] $command = $data\n")
  }
  if ($command eq 'init') {
    $FCCRECONNECT->{sec}=0;
    $FCCRECONNECT->{time}=time;
  }
  if ($command eq 'connect') {
    prout(" * Succesfully connected to the $COIN\-Server");
    if ($FCCINIT >= 65535) { $FCCINIT=16383 } else { $FCCINIT=3 }
    $FCCHANDSHAKE=1
  } elsif ($command eq 'input') {
    my $json;
    $EVALMODE=1;
    eval("\$json=decode_json(\$data)");
    $EVALMODE=0;
    if($@){
      prout("**WARNING** $COIN\-Server Posted not a Json String!\n$data\n$@")
    } else {
      my $cmd=$json->{command};
      my $func='cfcc_'.$cmd;
      if (defined(&$func)) { &$func($client,$json) }
      else { prout("**WARNING** $COIN\-Server Function '$cmd' not yet implemented!") }
    }
  } elsif ($command eq 'quit') {
    if ($FCCHANDLE) {
      if ($FCCRECONNECT->{sec}==0) {
        prout(" *!* ERROR: Lost the connection to the $COIN\-Server: $data");
        gclient::wsout($FCCHANDLE,$data,'close');
        $FCCHANDLE=undef
      }
    }
    $FCCRECONNECT={ sec => 10, time => time }
  } elsif ($command eq 'error') {
    prout(" *!!* $COIN\-Server responded with ERROR: $data")
  }
}

sub cfcc_error {
  my ($client,$k) = @_;
  prout(" *!!* $COIN\-Server responded with ERROR: $k->{error}")
}

sub cfcc_fcctime {
  my ($client,$k) = @_;
  setfcctime($k->{fcctime}-time);
  prout("> [$COIN] Time offset set to $FCCTIME\n");
  $FCCINIT |= 8;
}

sub cfcc_nodelist {
  my ($client,$k) = @_;
  my $num=1+$#{$k->{nodes}};
  prout("> [$COIN] The Core-network has $num nodes connected\n");
  foreach my $node (@{$k->{nodes}}) { addcandidate($node->{host},$node->{port}) }
  $FCCINIT |= 16;
}

sub cfcc_newnode {
  my ($client,$k) = @_;
  addcandidate($k->{host},$k->{port})
}

sub cfcc_init {
  my ($client,$k) = @_;
  prout(" !!! NODE INITIALISED AND ACTIVE !!!\n");
  outjsonparents({ command => 'ready' });
  $FCCHANDSHAKE=0; $FCCINIT = 65535
}

sub cfcc_calculatefee {
  my ($client,$k) = @_;
  outfcc({ command => 'calculatefee', totfee => calculatefee($k->{position},$k->{length}) })
}

sub cfcc_ledgerstatus {
  my ($client,$k) = @_;
  outfcc({ command => 'ledgerstatus', blockheight => $LASTBLOCK->{num}, ledgerlength => $LEDGERLEN })
}

sub cfcc_updatelist {
  my ($client,$k) = @_;
  my $dir=$INC{'gfio.pm'}; $dir =~ s/\\/\//g;
  my @sdir=split(/\//,$dir); pop @sdir;
  $UPDATEDIR=join("/",@sdir);
  $UPDATEFILES=[];
  foreach my $file (@{$k->{files}}) {
    my $fname="$UPDATEDIR/$file->{file}";
    if (-e $fname) {
      my $cont=gfio::content($fname);
      if (securehash($cont) ne $file->{fhash}) {
        my @stat=stat($fname);
        if ($stat[9] < $FCCTIME + $file->{mtime}) {
          push @$UPDATEFILES,$file->{file}
        }
      }
    } else {
      push @$UPDATEFILES,$file->{file}
    }
  }
  if ($#{$UPDATEFILES} < 0) {
    $UPDATEMODE=3;
  } else {
    outfcc({ command => 'updatefile', file => shift @$UPDATEFILES })
  }
}

sub cfcc_download {
  my ($client,$k) = @_;
  my $sign=$k->{file}.$k->{size}.$k->{data};
  if (Crypt::Ed25519::verify($sign,hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
    my $decoded=b64z($k->{data});
    if (length($decoded) == $k->{size}) {
      gfio::create("$UPDATEDIR/".$k->{file},$decoded);
      prout(" * Updated $UPDATEDIR/$k->{file}\n")
    } else {
      my $tlong=length($decoded) - $k->{size};
      prout(" * ERROR updating $UPDATEDIR/$k->{file}: Size Mismatch of $tlong - ");
    }
  } else {
    prout(" * ERROR updating $UPDATEDIR/$k->{file}: Signature Incorrect\n")
  }
  if ($#{$UPDATEFILES}<0) {
    gfio::create("update$FCCEXT",1);
    killserver("Restarting for updates")
  } else {
    my $file=shift @$UPDATEFILES;
    outfcc({ command => 'updatefile', file => $file });
    prout(" > Update File $file ...\n");
  }
}

sub cfcc_mine {
  my ($client,$k) = @_;
  foreach my $leaf (keys %$LEAVES) {
    if ($LEAVES->{$leaf}{fcc}{function} eq 'miner') {
      outjson($LEAVES->{$leaf},$k)
    }
  }
}

sub cfcc_solerr {
  my ($client,$k) = @_;
  my $miner = shift @SOLMINER;
  if ($miner) {
    if ($miner->{fcc}{wrongcount}) {
      $miner->{fcc}{wrongcount}++;
      if ($miner->{fcc}{wrongcount}>=5) {
        killclient($miner,"Too many wrong solutions tried");
        return 
      }
    } else {
      $miner->{fcc}{wrongcount}=1
    }
    print "Wrong Solution: $miner->{fcc}{wrongcount} solutions tried by $miner->{ip}:$miner->{port}\n";
    outjson($miner,{ command => 'wrong', message => "Illegal solution given" });
  }
}

sub cfcc_solution {
  my ($client,$k) = @_;
  my $miner = shift @SOLMINER;
  if ($miner) {
    $miner->{fcc}{wrongcount}=0;
    outjson($miner,{ command => 'solution' })
  }
}

sub signoutblockdata {
  my ($outblocks) = @_;
  my $sign="";
  foreach my $block (@$outblocks) {
    $sign.=$block->{wallet}.dechex($block->{amount},16).dechex($block->{fee},4);
    if ($block->{expire}) { $sign.=dechex($block->{expire},10) }
  }
  return $sign
}

sub addcoinbase {
  my ($data) = @_;
  foreach my $cb (@$COINBASELIST) {
    if ($cb->{signature} eq $data->{signature}) { return }
  }  
  push @$COINBASELIST,$data;
  if ($VOTING && ($VOTE->{transhash} eq $data->{signature})) {
    prout(" *> Adding coinbase to translist");
    # we missed this one, catching up
    $TRANSLIST->{$data->{signature}}={ coinbase => 1, transhash => $data->{signature} }
  }
}

sub cfcc_feepayout {
  my ($client,$k) = @_;
  my $sign=dechex($k->{spare},8) . dechex($k->{blockheight},12);
  $sign.=signoutblockdata($k->{outblocks});
  if (Crypt::Ed25519::verify($sign,hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
    addcoinbase($k)
  }
}

sub cfcc_coinbase {
  my ($client,$k) = @_;
  prout(" *> Coinbase received $k->{coincount}");
  my $sign=dechex($k->{coincount},8);
  $sign.=signoutblockdata($k->{outblocks});
  if (Crypt::Ed25519::verify($sign,hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
    addcoinbase($k)
  } else {
    prout(" *> Coinbase Signature failed")
  }
}

sub cfcc_message {
  my ($client,$k) = @_;
  if ($k->{message}) {
    if (Crypt::Ed25519::verify($k->{message},hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
      prout("\n================== $COIN Message ====================\n\n$k->{message}\n");
      prout("\n=====================================================\n");
    }
  }  
}

sub cfcc_shutdown {
  my ($client,$k) = @_;
  if ($k->{message}) {
    if (Crypt::Ed25519::verify($k->{message},hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
      gfio::create("update$FCCEXT",1);
      prout("\n================== Shutting Down ====================\n\n$k->{message}\n");
      prout("\n=====================================================\n");
      killserver("$COIN Reset requested");
    }
  }
}

sub cfcc_reset {
  my ($client,$k) = @_;
  if ($k->{message}) {
    if (Crypt::Ed25519::verify($k->{message},hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
      gfio::create("update$FCCEXT",1);
      prout("\n================= Resetting databases ===============\n\n$k->{message}\n");
      prout("\n=====================================================\n");
      FCC::fcc::killdb();
      killserver("$COIN Database Update requested");
    }
  }  
}

sub cfcc_resetip {
  my ($client,$k) = @_;
  if ($k->{mask} eq $SERVER->{fcc}{mask}) {
    if ($k->{message}) {
      if (Crypt::Ed25519::verify($k->{message},hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
        gfio::create("update$FCCEXT",1);
        killserver("$COIN Restart requested");
      }
    }
  }  
}

sub cfcc_hardreset {
  my ($client,$k) = @_;
  if ($k->{message}) {
    if (Crypt::Ed25519::verify($k->{message},hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
      gfio::create("update$FCCEXT",1);
      prout("\n================= Resetting Ledger ===============\n\n$k->{message}\n");
      prout("\n=====================================================\n");
      FCC::fcc::killdb();
      unlink("ledger$FCCEXT");
      killserver("$COIN Hard Restart requested");
    }
  }  
}

################# Node handling ##########################

sub goactive {
  outfcc({ 
    command=>'init',
    wallet=>$WALLET->{wallet},
    port=>$SERVER->{server}{port},
    cumhash=>$LASTBLOCK->{tcum},
    blockheight=>$LASTBLOCK->{num},
    ledgerlength=>$LEDGERLEN,
    version=>$FCCVERSION
  });
  $FCCINIT |= 32767;
}

sub statusmsg {
  if (time ne $STATUSTIME) {
   $STATUSTIME=time;
   my $inf=connectinfo();
   my $nparents=(1+$#{$inf->{parents}});
   my $nnodes=(1+$#{$inf->{nodes}});
   my $nleaves=(1+$#{$inf->{leaves}});
   my $nminers=(1+$#{$inf->{miners}});
   my $nunknowns=(1+$#{$inf->{unknowns}});
   my $ntrans=(1+$#{[keys %$TRANSLIST]});
   my $vote='X'; if ($VOTING) { $vote=$VOTE->{round} }
   my $sync=$LEDGERTODO ? int(($LEDGERTODO/($LEDGERWANTED||1))*100000)/1000 : 0;
   my @synca=split(/\./,$sync);
   $sync = $synca[0] . '.' . ($synca[1]//'') . ("0"x(3-length($synca[1]//'')));
   my $ibc=$IBC ? $IBC : 0;
   print STDOUT prtm(),
     ($nparents ? "Par=$nparents ":"").
     ($nnodes ? "Chld=$nnodes ":"").
     ($nleaves ? "Lvs=$nleaves ":"").
     ($nminers ? "Min=$nminers ":"").
     ($nunknowns ? "U=$nunknowns ":"").
     "Pnd=$ntrans Vote=$vote Ldg=$LEDGERLEN ".
     ($sync > 0 && $LASTBLOCK && $LASTBLOCK->{num} ? "Blk=$LASTBLOCK->{num} ":"").
     ($sync > 0 && $sync ? "Sync=$sync\% ":"").
     ($sync > 0 && $sync && $SER ? "SER=$SER ":"").
     ($sync > 0 && $ibc ? "IBC=$ibc ":"").
     "UT=".utmstr(time() - $SERVER->{starttime}).
     "    \r"
  }
}

sub serverloop {
  # The FCC Node Control Kernel
  if (!$SERVER) { die "Lost $COIN Server Handle" }
  my $ctm=gettimeofday();
  # parents are a bit different, since we are in non-loopmode for each client, where the server is in loopmode for each of it's clients
  if ($SHUTDOWNMODE) {
    my @tl=keys (%$TRANSLIST);
    if (!$VOTING && ($#tl<0) && !$TRANSDIST && ($#{$TRANSDISTLIST}<0)) {
      killserver("Shutting down for core-update .. $COIN Towards a brighter future!");
    }
  }
  statusmsg();
  my @nodes=(keys %$PARENTLIST); my $done=0;
  if ($#nodes >= 0) {
    if ($PARENTLOOP > $#nodes) { $PARENTLOOP=0 }
    my $start=$PARENTLOOP;
    do {
      my $node=$PARENTLIST->{$nodes[$start]};
      if ($node->{handle}) {
        $SERVERNODE=$node->{handle};
        $SERVERNODE->takeloop();
        $SERVERNODE=undef;
        $done=1
      } elsif (!$node->{connected} && ($#nodes<$MAXPARENTS-1)) {
        if ($node->{lastconnect} && ($ctm-$node->{lastconnect}>=10)) {
          prout("   - Retrying $node->{mask} .. ")
        } else {
          #prout("   - Connecting to $node->{mask} .. ")
        }
        addparent($nodes[$start]); $done=1
      } elsif ($node->{connected} && !$node->{fcc}{identified} && ($node->{lastconnect} && ($ctm-$node->{lastconnect}>=10))) {
        killclient($node->{handle},"408 Request TimeOut");
        prout("   x TimeOut $node->{mask} (no identify)\n")
      }
      $start++; if ($start>$#nodes) { $start=0 }
    } until ($done || ($start==$PARENTLOOP));
    $PARENTLOOP=$start
  }
  $CYCLELOOP++; my @ndl=keys %$NODES;
  if ($CYCLELOOP>=(1 + $#nodes)+(1 + $#ndl)) {
    $CYCLELOOP=0;
    if ($VOTING) { analysevotes() }
  }
  # check unknown timed-out clients
  $SERVER->broadcastfunc(\&checkhellotimeout,$ctm);
  # client jobs - one at a a time in the global loop to make it more fluent in execution
  my @leaves=(sort keys %$LEAVES);
  if ($#leaves >= 0) {
    if ($LEAFLOOP > $#leaves) { $LEAFLOOP=0 }
    my $client=$LEAVES->{$leaves[$LEAFLOOP]};
    $SERVERLEAF=$client;
    checkleafjob($client,$ctm);
    $SERVERLEAF=undef;
    $LEAFLOOP++
  }
  # CLEANING OLD SWAP TRANSACTION
  for my $s (keys %$SESSLIST) {
    for my $t (keys %{$SESSLIST->{$s}}) {
      if (time+$FCCTIME - $SESSLIST->{$s}{$t}{transaction}{fcctime} > 360 ) {
        prout "Timeout: Session $s Transaction $t\n";
        delete $SESSLIST->{$s}{$t};
        delete $SESSLIST->{$s};
      } # 5min timeout
    }
  }
  # transaction distribution
  if ($TRANSDIST) {
    my $fnd=0; my $nodes=corelist();
    my $th=substr($TRANSDISTDATA,-64);
    foreach my $node (@$nodes) {
      if (!$TRANSDISTDONE->{$node->{mask}.$th}) {
        outjson($node,{ command => 'transaction', data => $TRANSDISTDATA });
        $TRANSDISTDONE->{$node->{mask}.$th}=1;
        $fnd=1; last
      }
    }
    if (!$fnd) {
      $TRANSDIST=0; $TRANSLIST->{$th}{tobs}=0;
      foreach my $node (@$nodes) {
        delete $TRANSDISTDONE->{$node->{mask}.$th}
      }
    }
  } else {
    my @list=keys %$TRANSLIST;
    if (!$VOTING && (($#list>=0) || ($#{$COINBASELIST}>=0))) {
      if ($CYCLELOOP == 0) {
        votesuggest()
      }
    }
    if ($#{$TRANSDISTLIST}>=0) {
      $TRANSDISTDATA=shift @$TRANSDISTLIST; $TRANSDIST=1
    }
  }
  # fcc-server loop
  if ($FCCRECONNECT->{sec}>0) {
    if (time-$FCCRECONNECT->{time}>=10) {
      my $res=fccconnect($SERVER->{fcc}{localmode});
      if ($res) {
        $FCCRECONNECT->{sec}=0;
        if (minerspresent() && $FCCHANDSHAKE) {
          outfcc({ command => 'challenge' })
        }
      } else {
        $FCCRECONNECT->{time}=time+10
      }
    }
  } elsif ($FCCHANDLE) {
    $FCCHANDLE->takeloop();
  }
  # maintenance
  if ($ctm-$LASTCLIENTRUN>0.01) {
    # FCC-Server initisaling sequence
    # Initialise our node, connect to nodes, sync ledger
    if ($FCCINIT == 65535) {
      $LASTCLIENTRUN=$ctm; return
    } elsif ($FCCINIT == 0) {
      prout(" * Loading and verifying the ledger .. ");
      killserver("Rebuild Ledger Databases") if !load();
      prout(" * Ledger verified OK!\n");
      $LASTBLOCK=readlastblock(); $LEDGERLEN=0;
      if ($LASTBLOCK->{prev}) { $LEDGERLEN=$LASTBLOCK->{pos}+$LASTBLOCK->{next}+4 }
      $FCCINIT=1
    } elsif ($FCCINIT == 1) {
      fccconnect($SERVER->{fcc}{localmode});
      $FCCINIT=2;
    } elsif ($FCCINIT == 2) {
      if ($FCCHANDLE) { $FCCHANDLE->takeloop() }
      elsif (time-$FCCRECONNECT->{time}>5) {
        prout(" *!* ERROR: The $COIN\-Server seems to be offline ..\n");
        if (!-e "forward$FCCEXT") {
          prout(" -> You have not done a port-forwarding check yet\n -> You cannot do this while the $COIN\-Server is offline\n -> Please try again later!");
          killserver("Portforwarding check impossible"); exit
        }
        $FCCRECONNECT->{time}=time+10;
        if (-e "nodelist$FCCEXT") {
          my @nodes=split(/\n/,gfio::content("nodelist$FCCEXT"));
          if ($#nodes >= 0) {
            prout(" -> Retrieving nodes from stored nodelist ..\n");
            foreach my $node (@nodes) {
              addcandidate(split(/ /,$node))
            }
            $FCCINIT=31; return
          }
        }
        prout(" -> I have no way to find any nodes .. try again later!\n");
        killserver("Missing nodelist"); exit
      }
    } elsif ($FCCINIT == 3) {
      if ($UPDATEMODE >= 1) {
        if ($UPDATEMODE == 1) {
          prout(" * Checking for updates ..\n");
          outfcc({ command => 'updatelist' });
          $UPDATEMODE=2
        } elsif ($UPDATEMODE == 3) {
          if (-e "forward$FCCEXT") {
            $UPDATEMODE=0
          } else {
            # port forwarding check, need to be performed only once
            print " * Port forwarding check ..\n";
            outfcc({ command => 'callmeback', port => $SERVER->{fcc}{port} });
            $CALLBACKTIME = $ctm;
            $UPDATEMODE=4
          }
        } elsif ($UPDATEMODE == 4) {
          if ($ctm - $CALLBACKTIME >= 5) {
            portforwarding();
            killserver("Port forwarding disabled"); exit
          }
        }
      } else {
        prout(" * Checking time and nodelist\n");
        outfcc({ command => 'fcctime' });
        outfcc({ command => 'nodelist' });
        $FCCINIT=7
      }
    } elsif ($FCCINIT == 31) {
      # do we have nodes to sync the ledger?
      my @nodes=keys %$PARENTLIST; my $num=1+$#nodes;
      if (!$num) {
        prout(" * There are no active nodes in the pool\n");
        $FCCINIT=16383
      } else {
        prout(" * Accumulating nodes from the pool of $num nodes .. \n");
        $FCCINIT=63; $TRYTIME=$ctm
      }
    } elsif ($FCCINIT == 63) {
      # enough connected nodes to sync ledger?
      my $connected=0; my $tried=0;
      my @nodes=keys %$PARENTLIST; my $total=1+$#nodes;
      foreach my $mask (@nodes) {
        my $node=$PARENTLIST->{$mask};
        if ($node->{identified}) { $connected++ }
        if ($node->{tried}) { $tried++ }
      }
      if (($connected>=10) || ($connected>=$total) || (($tried>=$total) && ($ctm-$TRYTIME>10))) {
        $FCCINIT=127
      }
    } elsif ($FCCINIT == 127) {
      # collect information about the ledger
      prout(" * Syncing-process of the ledger data has started\n");
      my @nodes=(keys %$PARENTLIST); $REQUESTED=0;
      foreach my $mask (@nodes) {
        my $node=$PARENTLIST->{$mask};
        if ($node->{identified}) {
          outjson($node->{handle},{ command => 'ledgerinfo' });
          $REQUESTED++
        }
      }
      prout(" * Requested information from $REQUESTED nodes\n");
      $FCCINIT=255
    } elsif ($FCCINIT == 255) {
      # wait for ledgerinfo responses
      my @nodes=(keys %$PARENTLIST); my $total=0;
      foreach my $mask (@nodes) {
        my $node=$PARENTLIST->{$mask};
        if ($node->{ledgerlen}) { $total++ }
      }
      # parting nodes are handled in killclient, so every node should answer
      if ($total >= $REQUESTED) { $FCCINIT=511 } 
    } elsif ($FCCINIT == 511) {      
      my @responses=();
      my @nodes = sort { $PARENTLIST->{$b}{ledgerlen} <=> $PARENTLIST->{$a}{ledgerlen} } (keys %$PARENTLIST);
      foreach my $mask (@nodes) {
        my $node=$PARENTLIST->{$mask};
        if ($node->{ledgerlen}) {
          push @responses,$node->{ledgerlen}
        }
      }
      my $num=1+$#responses;
      if ($INFOMODE) {
        if ($INFOMODE == 1) {
          prout(" * INFORMATION MODE - Getting information from $num nodes.. ");
          $INFOTIME=$ctm; $INFOMODE=2
        } elsif ($INFOMODE == 2) {
          if ($ctm - $INFOTIME > 5) {
            my @nodes = sort { $PARENTLIST->{$b}{ledgerlen} <=> $PARENTLIST->{$a}{ledgerlen} } (keys %$PARENTLIST);
            if ($#nodes < 0) {
              prout(" !*! No nodes connected !");
              killserver("InfoMode completed");
              exit
            }
            my $cnt=1;
            foreach my $mask (@nodes) {
              my $node=$PARENTLIST->{$mask};
              prout(rsp($cnt,2).". ".rsp($node->{host},15)." ".rsp($node->{port},5)." ".rsp($node->{ledgerlen},12)." ".rsp($node->{blockheight},8)." ".substr($node->{lastcum},0,26));
              if ($cnt==1) { $INFOLL=$node->{ledgerlen}; $INFONODE=$node->{handle} }
              $cnt++
            }
            if ($INFODOWNLOAD) {
              $INFOMODE=3; $INFOPOS=0; gfio::create("ledger.download$FCCEXT","");
            } else {
              killserver("InfoMode completed");
              exit              
            }
          }
        } elsif ($INFOMODE == 3) {
          if (!$INFOREAD) {
            if ($INFOPOS>=$INFOLL) {
              prout(" * Done reading ledger.download$FCCEXT - $INFOLL bytes");
              killserver("InfoMode completed");
              exit
            }
            my $len=32768; if ($INFOLL<$INFOPOS+$len) { $len=$INFOLL-$INFOPOS }
            prout(" -> Reading $INFOPOS - $len");
            $INFOREAD=1;
            outjson($INFONODE,{ command => 'reqledger', pos => $INFOPOS, length => $len })
          }
        }
        return
      }
      prout(" * Evaluating responses from $num nodes .. ");
      if (!$num) { 
        prout(" * No nodes responded. Going into single-core-mode.");
        $FCCINIT=16383; return
      }
      if ($#nodes < 0) { 
        prout(" * Nodes that were present has quit during connection-phase\n * Going into single-core-mode.");
        $FCCINIT=16383; return
      }
      $LEDGERWANTED=$PARENTLIST->{$nodes[0]}{ledgerlen};
      my $todo = $LEDGERTODO = $LEDGERWANTED-$LEDGERLEN;
      if ($todo < 0) {
        prout(" * Our ledger is ahead, no syncing necessary\n");
        $FCCINIT=16383
      } elsif ($todo == 0) {
        prout(" * Ledger is perfectly synced\n");
        $FCCINIT=16383
      } else {
        prout(" * Syncing $todo bytes of $LEDGERWANTED bytes (".(int(($todo/$LEDGERWANTED)*100000)/1000)."\% left) \n");
        $FCCINIT=1023
      }
    } elsif ($FCCINIT == 1023) {
      # get ledgerdata
      if (!$SYNCING) {
        $SYNCPOS=$LEDGERLEN; $FIRSTSYNC=1; $SYNCING=1
      }
      syncledger();
    } elsif ($FCCINIT == 16383) {
      goactive();
    }
    $LASTCLIENTRUN=$ctm
  }
}

sub checkhellotimeout {
  my ($client,$ctm) = @_;
  if (!$client->{fcc}{identified}) {
    if ($ctm-$client->{fcc}{hellotime}>=5) {
      killclient($client,"Please identify yourself");
      firewall($client->{ip})
    }
  }
}


sub get_pending {
    my ($wallet) = @_;
    my $pubkey = publichash($wallet);
    my @incoming = ();
    my @outgoing = ();
    foreach my $th (keys %$TRANSLIST) {
        my $tx = $TRANSLIST->{$th};
        #print "Translist $th: ".gparse::str($tx)."\n";
        if (ref($tx) eq 'HASH' && !defined $tx->{coinbase} && defined $tx->{pubkey}) {
            if ($tx->{pubkey} eq $pubkey) {
                foreach my $out (@{$tx->{outblocks}}) {
                    if ($out->{wallet} ne $wallet) {  # Exclude change to self
                        push @outgoing, { transhash => $th, from => $wallet, to => $out->{wallet}, amount => $out->{amount} };
                    }
                }
            }
            foreach my $out (@{$tx->{outblocks}}) {
                if ($out->{wallet} eq $wallet) {
                    push @incoming, { transhash => $th, from => $tx->{wallet}, to => $wallet, amount => $out->{amount} };
                }
            }
        }
    }
    if ($VOTING) {
        my $vtx = $TRANSLIST->{$VOTE->{transhash}};
        if (ref($vtx) eq 'HASH' && !$vtx->{coinbase} && defined $vtx->{pubkey}) {
            if ($vtx->{pubkey} eq $pubkey) {
                foreach my $out (@{$vtx->{outblocks}}) {
                    if ($out->{wallet} ne $wallet) {  # Exclude change to self
                        push @outgoing, { transhash => $VOTE->{transhash}, from => $wallet, to => $out->{wallet}, amount => $out->{amount} };
                    }
                }
            }
            foreach my $out (@{$vtx->{outblocks}}) {
                if ($out->{wallet} eq $wallet) {
                    push @incoming, { transhash => $VOTE->{transhash}, from => $vtx->{wallet}, to => $wallet, amount => $out->{amount} };
                }
            }
        }
    }
    return (\@incoming, \@outgoing);
}


sub checkleafjob {
  my ($client,$ctm) = @_;
  if (!$client || !$client->{fcc} || !$client->{fcc}{identified}) { return }
  if ($#{$client->{fcc}{jobs}} >= 0) {
    my $job=shift @{$client->{fcc}{jobs}};
    if ($job->{command} eq 'balance') {
      my $balance = saldo($job->{wallet});
      my $spendable = get_max_spendable($job->{wallet},transinblocks($job->{wallet}));
      my ($incoming, $outgoing) = get_pending($job->{wallet});
      #print "Balance Reply: $job->{wallet} $job->{id} $balance\n";
      my $rpl = { 
        command => $job->{command}, 
        wallet => $job->{wallet}, 
        balance => $balance, 
        spendable => $spendable, 
        pending => { incoming => $incoming, outgoing => $outgoing } 
      };
      $rpl->{id} = $job->{id} if defined $job->{id};
      outjson($client,$rpl)
    }
    elsif ($job->{command} eq 'newtransaction') {
      my $wallet=createwalletaddress($job->{pubkey});
      my ($blocks,$change,$maxamount)=collectspendblocks($wallet,$job->{amount},transinblocks($wallet));
      # No Blocks to Spend
      if ($#{$blocks}<0) {
        my $rpl = { 
          command   => 'newtransaction', 
          transid   => $job->{transid}, 
          error     => "Insufficient funds", 
          available => $maxamount 
        };
        $rpl->{id} = $job->{id} if defined $job->{id};
        outjson($client,$rpl)
      }
      # Too many blocks needed to Spend full amount for max-in-blocks
      elsif ($#{$blocks}>255) {
        my $rpl = { 
          command   => 'newtransaction', 
          transid   => $job->{transid}, 
          error     => "Too many funds needed to make this transactions. Please split up into smaller amounts", 
          spendable => get_max_spendable($wallet,transinblocks($wallet))
        };
        $rpl->{id} = $job->{id} if defined $job->{id};
        outjson($client,$rpl)
      }
      else {
        if (!$job->{changewallet}) { $job->{changewallet}=$wallet }
        $job->{outsign} .= $job->{changewallet} . dechex($change,16) . '0000';
        my $iblocks = inblocklist($blocks);
        my $sign = join('',@$iblocks) . $job->{outsign};
        my $fcctime = newtrans($client,$job->{transid},$wallet,$job->{pubkey},$sign,$job->{numout});
        my %sess = (); $sess{sess}=$job->{sess} if $job->{sess};
        my $rpl = { 
          command   => 'newtransaction', 
          transid   => $job->{transid}, 
          sign      => $sign, 
          fcctime   => $fcctime,
          %sess
        };
        $rpl->{id} = $job->{id} if defined $job->{id};
        outjson($client,$rpl)
      }
    }
  }
}

sub getsyncpos {
  my $pos=$SYNCPOS; my $opos=$pos;
  do {
    $opos=$pos;
    foreach my $slot (keys %$SYNC) {
      if ($SYNC->{$slot}{pos} == $pos) {
        $pos+=$SYNC->{$slot}{length}
      }
    }
    while ($SYNCBUF->{$pos}) {
      $pos+=length($SYNCBUF->{$pos})
    }
  } until ($pos == $opos);
  return $pos
}

sub process_import_chunk {

  my $chunk_size = 32768 * 4;
  $chunk_size = $IMPORT_SIZE - $SYNCPOS if ($SYNCPOS + $chunk_size > $IMPORT_SIZE);

  $IMPORT_FH->seek($SYNCPOS);
  my $chunk = $IMPORT_FH->read($chunk_size);
  if (length($chunk) != $chunk_size) {
    prout(" * Fout bij lezen chunk op pos $SYNCPOS.");
    $IMPORT_ACTIVE = 0;
    if ($IMPORT_FH) { $IMPORT_FH->close; $IMPORT_FH = undef; }
    return;
  }

  my $first = ($SYNCPOS == $LEDGERLEN) ? 1 : 0;

  # Laat ledgerdata het werk doen (append + process blocks in chunk)
  if (!ledgerdata($chunk, $first)) {
    prout(" * Ongeldige chunk op pos $SYNCPOS—stop import.");
    $IMPORT_ACTIVE = 0;
    if ($IMPORT_FH) { $IMPORT_FH->close; $IMPORT_FH = undef; }
    return;
  }

  # Update globals (ledgerdata updatet intern via saveledgerdata)
  $SYNCPOS += $chunk_size;
  $LASTBLOCK = readlastblock();
  $LEDGERLEN = $LASTBLOCK->{pos} + $LASTBLOCK->{next} + 4;
  $LEDGERTODO = $LEDGERWANTED-$LEDGERLEN;

}

sub syncledger {

  # Import-Sync
  if (-e $IMPORT_FILE) {
    $IMPORT_SIZE = -s $IMPORT_FILE;
    if ($IMPORT_SIZE > $LEDGERLEN) {
      if (!$IMPORT_FH) {
        prout(" * Start file-sync: delta " . ($IMPORT_SIZE - $LEDGERLEN) . " bytes.");
        $IMPORT_FH = gfio::open($IMPORT_FILE);
        $LEDGERWANTED = $IMPORT_SIZE;
        $SYNCPOS = $LEDGERLEN;  # Start bij einde
        $IMPORT_ACTIVE = 1;
        $FIRSTSYNC = 1;
        $LOGMODE = 1;
      }
      process_import_chunk();  # Één chunk per call
      return;  # Laat loop doorlopen
    }
    elsif ($IMPORT_FH) {
      prout(" * Import voltooid (EOF bereikt): Ledger nu $LEDGERLEN bytes.");
      $IMPORT_FH->close;
      $IMPORT_FH = undef;
      allowsave();
      save();  # Save DBs na import
      prout(" * Databases gesaved na import.");
      $IMPORT_ACTIVE = 0;
      killserver("Restarting for Node-Sync mode");
      return
    }
  }

  # Node-Sync
  my $tm = gettimeofday();
  my $pos = getsyncpos();
  my @nodes = randomarray(keys %$PARENTLIST);
  if ($#nodes < 0) {
    prout(" *!* No nodes left to sync with .. advisable to restart");
    killserver("Error syncing");
    exit
  }

  # Al pending – check als genoeg responses voor majority check
  if ($LAST_SYNC_POS) {
    my $responses = $SYNC_CHECKS->{$LAST_SYNC_POS} // [];
    if (scalar(@$responses) >= $MIN_CONFIRM_NODES) {
      my $hash_count = {};
      my $node_by_hash = {};  # Voor fault bad nodes
      foreach my $resp (@$responses) {
        $hash_count->{$resp->{hash}}++;
        $node_by_hash->{$resp->{hash}} //= [];
        push @{$node_by_hash->{$resp->{hash}}}, $resp->{client};
      }

      my @sorted_hashes = sort { $hash_count->{$b} <=> $hash_count->{$a} } keys %$hash_count;
      my $majority_hash = $sorted_hashes[0];
      my $majority_count = $hash_count->{$majority_hash};

      if ($majority_count > scalar(@$responses) / 2) {
        # Majority: Neem data van eerste match
        my ($good_data) = grep { $_->{hash} eq $majority_hash } @$responses;
        my $first = ($SYNCPOS == $LEDGERLEN) ? 1 : 0;
        if (ledgerdata($good_data->{data}, $first)) {
          $SYNCPOS += length($good_data->{data});
          $LASTBLOCK = readlastblock();
          $LEDGERLEN = $LASTBLOCK->{pos} + $LASTBLOCK->{next} + 4;
          if ($LEDGERLEN >= $LEDGERWANTED) {
            $FCCINIT = 16383;
            $SYNCING = 0;
          }
          #prout(" * Pos $LAST_SYNC_POS confirmed by majority $majority_count – appended.");
          # Fault minority nodes
          #foreach my $bad_hash (@sorted_hashes[1..$#sorted_hashes]) {
          #  foreach my $bad_client (@{$node_by_hash->{$bad_hash}}) {
          #    fault($bad_client);
          #  }
          #}
          delete $SYNC_CHECKS->{$LAST_SYNC_POS};  # Clear na succes
          $LAST_SYNC_POS=undef;
        } else {
          prout(" * Majority $majority_count data for pos $LAST_SYNC_POS invalid – retry.  ");
          delete $SYNC_CHECKS->{$LAST_SYNC_POS};  # Clear voor retry
          $pos = $LAST_SYNC_POS;
          $LAST_SYNC_POS=undef;
        }
      } else {
        prout(" * No majority $majority_count for pos $LAST_SYNC_POS – retry.");
        delete $SYNC_CHECKS->{$LAST_SYNC_POS};  # Clear voor retry
        $pos = $LAST_SYNC_POS;
        $LAST_SYNC_POS=undef;
      }
    }
  }

  my $len = 32768;
  if ($pos + $len >= $LEDGERWANTED) { $len=$LEDGERWANTED - $pos; }

  if ($len > 0 && !$LAST_SYNC_POS) {
    $LAST_SYNC_POS = $pos;
    # Bijhouden of pos pending is (responses wachten)
    if (! $SYNC_CHECKS->{$pos} || scalar(@{$SYNC_CHECKS->{$pos}}) < $MIN_CONFIRM_NODES) {
      # Nog niet (genoeg) requested/responses – request 3 random !busy nodes
      $SYNC_CHECKS->{$pos} = [] unless $SYNC_CHECKS->{$pos};  # Init buffer
      my $requested = 0;
      foreach my $mask (@nodes) {
        my $node = $PARENTLIST->{$mask};
        if ($node->{identified} && (!$SYNC->{$mask} || !$SYNC->{$mask}{busy})) {
          $SYNC->{$mask} = {
            ok => 1,
            node => $node,
            busy => 1,
            ready => 0,
            pos => $pos,
            length => $len,
            data => "",
            time => $tm,
            first => $FIRSTSYNC
          };
          outjson($node->{handle}, { command => 'reqledger', pos => $pos, length => $len });
          #prout(" * Request $pos – $mask");
          $requested++;
          if ($requested >= $MIN_CONFIRM_NODES) { last; }
        }
      }
      if ($requested < $MIN_CONFIRM_NODES) {
        prout(" * Niet genoeg nodes voor pos $pos – wacht of retry later.");
      }
      $FIRSTSYNC=0;
    }
  }

  $LEDGERTODO = $LEDGERWANTED-$LEDGERLEN;

  foreach my $slot (keys %$SYNC) {
    if (!$SYNC->{$slot}{ok}) { delete $SYNC->{$slot} }
  }

}

sub randomarray {
  my @array = @_; my @list=();
  while ($#array > -1) {
    if (rand > 0.5) { push @list, shift(@array) }
    else { unshift @list, pop(@array) }
  }
  return @list
}

sub prin {
  my ($func,$command,$data) = @_;
  my $out = "SERV <IN ($func): $command - ";
  $EVALMODE=1;
  my $msg; eval { $msg=decode_json($data) };
  $EVALMODE=0;
  if ($@) { prout($out.$data."\n"); return }
  if (ref($msg) eq 'HASH') {
    my @out=();
    foreach my $k (sort keys %$msg) {
      my $v=$msg->{$k};
      if (ref($v) eq 'HASH') {
        push @out,"$k={".join(", ",sort (keys %$v))."}"
      } elsif (ref($v) eq 'ARRAY') {
        my $num=$#{$v};
        push @out,"$k=ARRAY[$num]"
      } elsif (defined($v)) {
        if (length($v)>500) { $v=substr($v,0,500)." ..." }
        push @out,"$k=$v"
      } else {
        push @out,"$k=[undef]"
      }
    }
    prout($out,join(", ",@out),"\n")
  } else {
    prout($out,"$data\n")
  }
}

sub handleserver {
  # Incoming message from a client we serve to
  my ($client,$command,$data) = @_;
  if ($client->{killed}) { return }
  if ($command eq 'init') {
    prout("Init $client->{ip}\n");
    if (!$CLIENTIP->{$client->{ip}}) {
      $CLIENTIP->{$client->{ip}}=1;
    } else {
      $CLIENTIP->{$client->{ip}}++;
      if ($CLIENTIP->{$client->{ip}} > 5) {
        killclient($client,"Maximum connections on this IP exceeded");
        return
      }
    }
  }
  elsif ($command eq 'sent') {
    $DATASENT+=$data; return
  } elsif ($command eq 'received') {
    $DATARECEIVED+=$data; return
  } elsif ($command eq 'connect') {    
    $client->{ischild}=1;
    $client->{mask}=$client->{ip}.":????";
    my $ctm=gettimeofday();
    $client->{fcc} = { function => 'unknown', faults => 0, identified => 0, hellotime => $ctm };
  }
  if ($DEBUG && ($command ne 'loop') && ($command ne 'noinput')) {
    if (!$data) { $data="" }
    my $func=$client->{fcc}{function};
    if (!$func) { $func='unknown client' }
    prin($func,$command,$data)
  }
  if ($command eq 'quit') {
    killclient($client,$data)
  } elsif ($command eq 'error') {
    killclient($client,$data)
  } elsif ($command eq 'handshake') {    
    if ($FCCINIT < 65535) {
      if (($FCCINIT == 3) && ($UPDATEMODE == 4)) {
        print " < Callback connection established\n";
      } else {
        prout("   x illegal connect $client->{ip}\n");
        killclient($client,'Trying to connect to non initialised node');
      }
    } else {
      prout("   + connected $client->{ip}\n");
      outjson($client,{ command => 'hello', version => $FCCVERSION, host=>$SERVER->{fcc}{host}, port=>$SERVER->{fcc}{port} });
    }
  } elsif ($command eq 'input') {
    $EVALMODE=1;
    my $k; eval { $k=decode_json($data) };
    $EVALMODE=0;
    if ($@) {
      prout("Illegal data received from $client->{ip}:$client->{port}: $data\n");
      killclient($client,$data); return
    }    
    my $cmd=$k->{command};
    if (!$cmd) {
      prout("Illegal data (no command in JSON) received from $client->{ip}:$client->{port}\n");
      killclient($client,$data); return
    }
    my $func="c_$cmd";
    if (defined &$func) {
      &$func($client,$k)
    } else {
      prout("Illegal JSON-command '$cmd' received from $client->{ip}:$client->{port}\n");
      outjson($client,{ command=>'error', error=>"Unknown command given in input" }); 
      fault($client)
    }
  }
}

sub handlenode {
  # Incoming message from a node we are client to
  my ($node,$cmd,$msg) = @_;
  if (!$node || $node->{error} || $node->{killed}) { return }
  if (!$msg) { $msg="" }
  my $mask=$node->{mask};
  if (!$mask) { $mask='unknown' }
  if ($DEBUG && ($cmd ne 'loop') && ($cmd ne 'noinput')) {
    prin('parent',$cmd,$msg)
  }
  if ($cmd eq 'connect') {
    $PARENTLIST->{$mask}{connected}=1
  } elsif ($cmd eq 'input') {
    if (!$msg) { return }
    $EVALMODE=1;
    my $k; eval { $k=decode_json($msg) };
    $EVALMODE=0;
    if ($@) {
      prout(" !*! JSON error ($mask): $@\n");
      killclient($node,"JSON error: $@\n")
    }
    my $cmd=$k->{command};
    if (!$cmd) { return }
    my $proc="c_$cmd";
    if (defined &$proc) {
      &$proc($node,$k)
    } else {
      prout("Illegal command received from $mask: $cmd\n");
      fault($node)
    }
  } elsif ($cmd eq 'error') {
    prout("Lost connection to node $mask: $msg\n");
    killclient($node,$msg);
  } elsif ($cmd eq 'quit') {
    if ($FCCINIT == 255) { $REQUESTED-- }
    prout("Node $mask terminated: $msg\n");
    killclient($node,$msg);
  }
}

sub bcn {
  my ($client,$json) = @_;
  if ($client->{fcc} && ($client->{fcc}{function} eq 'node')) {
    wsmessage($client,$json)
  }
}

sub c_quit {
  my ($client) = @_;
  killclient($client)
}

sub c_error {
  my ($client,$k) = @_;
  killclient($client,$k->{message},1);
}

###################### handshake ##############################

sub c_hello {
  my ($client,$k) = @_;
  if (!$k->{build}) { $k->{build} = '1.01' }
  my $mask=join(':',$k->{host},$k->{port});
  if ($client->{ischild}) { 
    prout(" !*! Rejected! Child-Node $client->{mask} tried to identify as a parent !*!\n");
    killclient($client,"Don't hack"); return
  }
  if ($client->{mask} ne $mask) {
    prout(" !*! Rejected! Parent-Node $client->{mask} tried to identify as $mask !*!\n");
    killclient($client,"Don't hack"); return
  }
  if ($k->{build} eq '2.3.1') {
    prout(" !*! Rejected! Node $client->{mask} is running buggy version 2.3.1 !*!\n");
    killclient($client,"Version 2.3.1 is rejected by this node because of bugs"); return
  }
  if ($DEBUG) {
    prout("Response from node $client->{mask} -> Identify as node\n");
  }
  if ($k->{version} gt $FCCVERSION) {
    prout("! Node $client->{mask} has version $k->{version}, we only have version $FCCVERSION");
    killclient($client,"Version $k->{version} is not supported by this node");
    return    
  }
  $client->{fcc}{port}=$k->{port};
  $client->{mask}=$mask;
  if (!$PARENTLIST->{$client->{mask}}) {
    prout("! The port the node gave us is unknown in the core-list");
    killclient($client,"The port the node gave us is unknown in the core-list");
    return
  }
  $client->{fcc}{version}=$k->{version};
  $client->{fcc}{build}=$k->{build};
  $client->{fcc}{entrytime} = time+$FCCTIME;
  my $send= {
    command => 'identify',
    type => 'node',
    version => $FCCVERSION,
    build => $FCCBUILD,
    host => $SERVER->{fcc}{host},
    port => $SERVER->{fcc}{port}
  };
  $client->{fcc}{function}='node';
  $client->{fcc}{identified}=1;
  $PARENTLIST->{$client->{mask}}{identified}=1;
  addnodelist($client->{host},$client->{fcc}{port});
  outjson($client,$send);
  if ($FCCINIT == 65535) {
    outjson($client,{ command => 'ready' });
  }
  $client->outburst();
}

sub c_callback {
  my ($client,$k) = @_;
  if (Crypt::Ed25519::verify($SERVER->{fcc}{host}.'callback',hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
    prout(" * Callback received !! Your portforwarding is active !!");
    gfio::create("forward$FCCEXT",1);
    $UPDATEMODE=0;
    killclient($client,"Succesful callback")
  } else {
    killclient($client,"Illegal callback")
  }
}

sub c_identify {
  my ($client,$k) = @_;
  if (!$k->{build}) { $k->{build} = '1.01' }
  if ($SHUTDOWNMODE) { killclient($client,"Service temporarely unavailable"); return }
  if ($k->{build} eq '2.3.1') {
    prout(" !*! Rejected! Node $client->{mask} is running buggy version 2.3.1 !*!\n");
    killclient($client,"Version 2.3.1 is rejected by this node because of bugs"); return
  }
  if ($k->{type} eq 'node') {
    my $mask=join(':',$k->{host},$k->{port});
    $client->{mask}=join(':',$client->{ip},$k->{port});
    if ($client->{mask} ne $mask) {
      prout(" !*! Rejected! Child-Node $client->{mask} tried to identify as $mask !*!\n");
      killclient($client,"Don't hack"); return
    }
  } elsif ($client->{isparent}) {
    prout(" !*! Rejected! Parent-Node $client->{mask} tried to identify as a child !*!\n");
    killclient($client,"Don't hack"); return
  } else {
    $client->{mask}=join(':',$client->{ip},"[".$client->{port}."]");
    $client->{fcc}{jobs}=[];
    $LEAVES->{$client->{mask}}=$client
  }
  $client->{fcc}{identified}=1;
  $client->{fcc}{function}=$k->{type};
  $client->{fcc}{version}=$k->{version};
  $client->{fcc}{build}=$k->{build};
  if ($k->{type} eq 'node') {
    $client->{fcc}{port}=$k->{port};
    if (!checklanwan($client->{ip})) {
      killclient($client,"LAN/WAN Intrucion"); return
    }
    $NODES->{$client->{mask}}=$client;
  }
  if ($k->{version} gt $FCCVERSION) {
    # assume backwards-compatiblity
    prout(" !*! Client $client->{mask} is running version $k->{version}. We only $FCCVERSION!\n");
    killclient($client,"Version $k->{version} is not supported by this node");
    return
  }
  if ($k->{type} eq 'miner') {
    outfcc({ command => 'challenge' }) 
  }
  $client->{fcc}{version}=$k->{version};
  prout("Client $client->{mask} is a $k->{type} v$k->{build} ($k->{version})\n");
}

sub c_identifyme {
  my ($client,$k) = @_;
  return if $client->{fcc}{identified};
  c_identify($client,$k);
}

sub c_ready {
  my ($client) = @_;
  if (!$client->{fcc} || !$client->{fcc}{port} || ($client->{fcc}{function} ne 'node')) { killclient($client,"H4x0rz"); return }
  prout(" *> Node $client->{ip}:$client->{port} is identified");
  $client->{fcc}{ready}=1;
}

sub c_transaction {
  my ($client,$k) = @_;
  if ($FCCINIT < 65535) { return }
  if (!$k->{data} || !$client->{fcc} || !$client->{fcc}{port} || ($client->{fcc}{function} ne 'node')) { killclient($client,"H4x0rz"); return }
  if (!$client->{fcc}{isparent} && !$client->{fcc}{ready}) { killclient($client,"H4x0rz"); return }
  # here we check for illegal voting
  my $trans=addtransaction($client,$k->{data});  
  if (!$trans->{nocheck}) {
    if ($trans->{error}) {
      prout(" !* Illegal transaction received from $client->{mask} - $trans->{error}");
      if ($VOTE->{transhash} && $trans->{transhash} && $VOTE->{transhash} eq $trans->{transhash}) {
        $VOTE->{illegal}=1
      }
    }
  }
}

sub c_coinbasetrans {
  my ($client,$k) = @_;
  if ($FCCINIT < 65535) { return }
  $k=$k->{data};
  if (!$k || !$k->{coincount} || !$k->{outblocks} || !$k->{signature}) {
    fault($client);
    prout(" !*! Illegal coinbase $client->{ip}:$client->{port}"); return
  }
  my $sign=dechex($k->{coincount},8);
  $sign.=signoutblockdata($k->{outblocks});
  if (Crypt::Ed25519::verify($sign,hexoct($FCCSERVERKEY),hexoct($k->{signature}))) {
    addcoinbase($k);
    $TRANSLIST->{$k->{signature}}={ coinbase => 1, transhash => $k->{signature} }
  }
}

##################### mining ######################################

sub c_solution {
  my ($client,$k) = @_;
  push @SOLMINER,$client;
  outfcc({ command => 'solution', wallet => $k->{wallet}, solhash => $k->{solhash} })
}

sub c_challenge {
  my ($client)=@_;
}

####################### Sync Ledger ############################################

sub c_reqtrans {
  my ($client,$k) = @_;
  if ($FCCINIT < 65535) { return }
  if ($TRANSLIST->{$k->{transhash}}) {
    if (length($k->{transhash}) == 128) {
      # coinbase request
      foreach my $cb (@$COINBASELIST) {
        if ($cb->{signature} eq $k->{transhash}) {
          outjson($client, { command => 'coinbasetrans', data => $cb }); return
        }
      }
    }
    outjson($client,{ command => 'transaction', data => $TRANSLIST->{$k->{transhash}}{data} })
  }
}

sub c_ledgerinfo {
  my ($client,$k) = @_;
  if ($FCCINIT<255) { return }
  outjson($client,{ command=>'ledgerresponse', size=>$LEDGERLEN, height=>$LASTBLOCK->{num}, cumhash=>$LASTBLOCK->{tcum} })
}

sub c_ledgerresponse {
  my ($client,$k) = @_;
  if ($client->{ischild}) { return }
  my $node=$PARENTLIST->{$client->{mask}};
  $node->{ledgerlen}=$k->{size};
  $node->{lastcum}=$k->{cumhash};
  $node->{blockheight}=$k->{height};
  if ($SYNC->{$client->{mask}}) {
    $SYNC->{$client->{mask}}{busy}=0
  }
}

sub old_c_reqledger {
  my ($client,$k) = @_;
  if ($FCCINIT < 65535) { return }
  if ($k->{pos}+$k->{length}>$LEDGERLEN) {
    fault($client); return
  }
  my $sz=-s "ledger$FCCEXT";
  if ($sz<$k->{pos}+$k->{length}) {
    outjson($client,{ command => 'ledgerdata', error => 'Cannot supply block', pos => $k->{pos}, data => "", final => $k->{final} || 0, first => $k->{first} || 0 })
  } else {
    my $data=zb64(gfio::content("ledger$FCCEXT",$k->{pos},$k->{length}));
    outjson($client,{ command => "ledgerdata", pos => $k->{pos}, data => $data, final => $k->{final} || 0, first => $k->{first} || 0 })
  }
}

# Geupdate c_reqledger: Voeg checksum toe voor new nodes
sub c_reqledger {
  my ($client,$k) = @_;
  if ($FCCINIT < 65535) { return }
  if ($k->{pos}+$k->{length}>$LEDGERLEN) {
    fault($client); return
  }
  my $sz=-s "ledger$FCCEXT";
  if ($sz<$k->{pos}+$k->{length}) {
    outjson($client,{ command => 'ledgerdata', error => 'Cannot supply block', pos => $k->{pos}, data => "", final => $k->{final} || 0, first => $k->{first} || 0 })
  } else {
    my $raw_chunk = gfio::content("ledger$FCCEXT",$k->{pos},$k->{length});
    my $chunk_hash = securehash($raw_chunk);  # Checksum voor validatie
    my $data=zb64($raw_chunk);
    outjson($client,{ command => "ledgerdata", pos => $k->{pos}, data => $data, chunk_hash => $chunk_hash, final => $k->{final} || 0, first => $k->{first} || 0 })
  }
}

sub c_ledgerdata {
  my ($client,$k) = @_;
  if ($k->{error}) {
    killclient($client,"Desynced ($k->{error})"); return
  }
  my $decoded = b64z($k->{data});
  my $calc_hash = securehash($decoded);  # Altijd bereken

  # Checksum check (backwards compat)
  if ($k->{chunk_hash}) {
    if ($calc_hash ne $k->{chunk_hash}) {
      prout(" * Checksum mismatch from $client->{mask} – discard.");
      fault($client);
      if ($SYNC->{$client->{mask}}) {
        $SYNC->{$client->{mask}}{busy} = 0;  # Reset busy bij fail
      }
      return;
    }
  }

  # Buffer in $SYNC_CHECKS voor pos, incl. client voor fault
  my $pos = $k->{pos};
  push @{$SYNC_CHECKS->{$pos}}, { 
    mask => $client->{mask}, 
    data => $decoded, 
    hash => $calc_hash, 
    client => $client 
  };

  # Reset busy voor deze mask na response (klaar voor volgende)
  if ($SYNC->{$client->{mask}}) {
    $SYNC->{$client->{mask}}{busy} = 0;
    $SYNC->{$client->{mask}}{ready} = 1;  # Mark ready voor evaluatie
  }
}



##################### LEAF ##########################################

sub c_balance {
  my ($client,$k) = @_;
  my $error="";
  if ($SHUTDOWNMODE) {
    print "Rebooting, client killed\n";
    killclient($client,"Service temporarely unavailable"); 
    return 
  }
  $k->{id} = $k->{id} // time()-$FCCTIME;
  if ($FCCINIT<65535) {
    $error="I'm not initialised yet";
  } elsif (!$client->{fcc} || !$client->{fcc}{identified}) {
    $error="You are not identified yet";
  } elsif (!$client->{fcc}{function} eq 'leaf') {
    $error="You are not connected as a leaf"
  } elsif (!validwallet($k->{wallet})) {
    $error="Invalid wallet given"
  } elsif ($#{$client->{fcc}{jobs}} >= 4) {
    $error="Too many requests"
  }
  if ($error) {
    #print "Balance ERROR: $error\n";
    my $rpl = { command => 'balance', error => $error };
    $rpl->{id} = $k->{id} if defined $k->{id};
    outjson($client,$rpl)
  } else {
    #print "Balance Job: $k->{wallet} $k->{id}\n";
    my $rpl = { command => 'balance', wallet => $k->{wallet} };
    $rpl->{id} = $k->{id} if defined $k->{id};
    push @{$client->{fcc}{jobs}},$rpl
  }
}

sub c_history {
}

sub c_newtransaction {
  my ($client,$k) = @_;
  if ($SHUTDOWNMODE) { killclient($client,"Service temporarely unavailable"); return }
  my $error=""; my $amount=0; my $outsign="";
  if ($FCCINIT<65535) {
    $error="I'm not initialised yet"
  } elsif (!$k->{transid}) {
    $error="No transaction-identification given"
  } elsif ($TRESLIST->{$client->{mask}} && ($TRESLIST->{$client->{mask}}{$k->{transid}})) {
    $error="transaction-identification already exists"
  } elsif (!$k->{pubkey} || !validh64($k->{pubkey})) {
    $error="Invalid public key given"
  } elsif ($BLOCKLIST->{$k->{pubkey}}) {
    $error="You are banned from making transactions, bad scriptkiddo!"
  } elsif (!$client->{fcc} || !$client->{fcc}{identified}) {
    $error="You are not identified yet"
  } elsif ($client->{fcc}{function} ne 'leaf') {
    $error="You are not connected as a leaf"
  } elsif (!$k->{to} || (ref($k->{to}) ne 'ARRAY')) {
    $error="Invalid to-spend-list given"
  } elsif ($#{$k->{to}}>=255) {
    $error="Too many recipients in to-spend-list (max=255)"
  } elsif ($k->{changewallet} && !validwallet($k->{changewallet})) {
    $error="Invalid Change-wallet given"
  } else {
    foreach my $to (@{$k->{to}}) {
      if ((ref($to) ne 'HASH') || (!validwallet($to->{wallet})) || (!$to->{amount}) || ($to->{amount} =~ /[^0-9]/)) {
        $error="Invalid spend-block in to-spend-list"; last
      } elsif ((!$to->{fee}) || ($to->{fee} =~ /[^0-9]/) || ($to->{fee}<$MINIMUMFEE)) {
        if (!checkgenesis($k->{pubkey})) {
          $to->{fee}=$MINIMUMFEE
        } else {
          $to->{fee}=0
        }
      }
      my $afee=doggyfee($to->{amount},$to->{fee});
      $amount+=$to->{amount}+$afee;
      $outsign.=$to->{wallet}.dechex($to->{amount},16).dechex($to->{fee},4)
    }
  }
  if (!$amount && !$error) { $error="No orders given in to-spend-list" }
  if ($error) {
    my $rpl = { command => 'newtransaction', transid => $k->{transid}, error => $error };
    $rpl->{id} = $k->{id} if defined $k->{id};
    outjson($client,$rpl)
  } else {
    my %sess = (); $sess{sess}=securehash((time+$FCCTIME).$k->{pubkey}.$k->{transid}) if $k->{sess};
    $client->{sess} = $sess{sess} if $k->{sess};
    prout "Create Session $client->{sess} with New Transaction $k->{transid}\n" if $k->{sess};
    my $rpl = {
      command => 'newtransaction',
      pubkey => $k->{pubkey},
      amount => $amount,
      transid => $k->{transid},
      changewallet => $k->{changewallet} || "",
      outsign => $outsign,
      numout => 2+$#{$k->{to}},
      %sess
    };
    $rpl->{id} = $k->{id} if defined $k->{id};
    push @{$client->{fcc}{jobs}},$rpl
  }
}

sub c_signtransaction {
  my ($client,$k) = @_;
  my $error=""; 
  my $hash="";
  my $mask = $k->{sess} && $SESSLIST->{$k->{sess}} && $SESSLIST->{$k->{sess}}{$k->{transid}} ? $SESSLIST->{$k->{sess}}{$k->{transid}}{mask} : $client->{mask};
  prout ("Reopen Session $k->{sess} mask $mask\n") if $k->{sess};
  if ($FCCINIT<65535) {
    $error="I'm not initialised yet";
    prout ("ERROR: I'm not initialised yet for Session $k->{sess}\n") if $k->{sess};
  } elsif (!$client->{fcc} || !$client->{fcc}{identified}) {
    $error="You are not identified yet";
    prout ("ERROR: You are not identified yet for Session $k->{sess}\n") if $k->{sess};
  } elsif (!$client->{fcc}{function} eq 'leaf') {
    $error="You are not connected as a leaf";
    prout ("ERROR: You are not connected as a leaf for Session $k->{sess}\n") if $k->{sess};
  } elsif (!$k->{signature} || ($k->{signature} =~ /[^0-9A-F]/) || (length($k->{signature}) != 128)) {
    $error="Illegal signature syntax";
    prout ("ERROR: Illegal signature syntax for Session $k->{sess}\n") if $k->{sess};
  } elsif (!$k->{transid} || $k->{transid} =~ /[^0-9]/) {
    $k->{transid}='error';
    $error="Illegal transaction id";
    prout ("ERROR: Illegal transaction id Session $k->{sess}\n") if $k->{sess};
  } else {
    if ($k->{sess} && (!defined $SESSLIST->{$k->{sess}} || !defined $SESSLIST->{$k->{sess}}{$k->{transid}})) {
      $error="Transaction Session with id $k->{transid} is unknown";
      prout ("ERROR: Unknown Session $k->{sess}\n") if $k->{sess};
    } elsif (!$k->{sess} && (!$TRESLIST->{$mask} || !$TRESLIST->{$mask}{$k->{transid}})) {
      $error="Transaction with id $k->{transid} is unknown"
    } else {
      my $trans = $k->{sess} ? $SESSLIST->{$k->{sess}}{$k->{transid}}{transaction} : $TRESLIST->{$mask}{$k->{transid}};
      prout ("Sign Session $k->{sess} Transaction $trans->{transid}\n") if $k->{sess};
      if ($k->{sess} && !Crypt::Ed25519::verify($k->{sess},hexoct($trans->{pubkey}),hexoct($k->{sesssignature}))) {
        $error="Ed25519 Verification mismatch on session signature";
      } elsif ($k->{sess} && $k->{wallet} && substr($trans->{signdata},-176,68) ne $k->{wallet}) {
        $error="Session Wallet Verification mismatch on transaction";
        prout ("ERROR: Session Wallet Verification mismatch on transaction\n") if $k->{sess};
      } elsif ($k->{sess} && $k->{amount} && hexdec(substr($trans->{signdata},-108,16)) != $k->{amount}) {
        $error="Session Amount Verification mismatch on transaction";
        prout ("ERROR: Session Amount Verification mismatch on transaction\n") if $k->{sess};
      } elsif (!Crypt::Ed25519::verify($trans->{signdata},hexoct($trans->{pubkey}),hexoct($k->{signature}))) {
        $error="Ed25519 Verification mismatch on signature";
        prout ("ERROR: Ed25519 Verification mismatch on signature\n") if $k->{sess};
      } else {
        # Broadcast transaction!
        my $data=dechex($trans->{fcctime},8).$trans->{pubkey}.dechex($trans->{numout},2).$trans->{signdata}.$k->{signature};
        $hash=securehash($data); $data.=$hash;
        #addtransaction($client,$data,1);
        my $trans=addtransaction($client,$data);
        $trans->{id} = $k->{id} if defined $k->{id};
        if ($trans->{error}) { 
          $error=$trans->{error};
          prout ("Add Transaction ERROR: $error\n") if $k->{sess};
        }
        elsif ($k->{sess}) {
          prout "Added Session $k->{sess} Transaction $k->{transid}\n" if $k->{sess};
          delete $SESSLIST->{$k->{sess}}{$k->{transid}};
          delete $SESSLIST->{$k->{sess}};
        }
      }
      delete $TRESLIST->{$mask}{$k->{transid}}
    }
  }
  if ($error) {
    my $rpl = { command => 'signtransaction', transid => $k->{transid}, error => $error };
    $rpl->{id} = $k->{id} if defined $k->{id};
    outjson($client,$rpl)
  } else {
    my $rpl = { command => 'signtransaction', transid => $k->{transid}, transhash => $hash};
    $rpl->{id} = $k->{id} if defined $k->{id};
    outjson($client,$rpl)
  }
}

############ voting response #######################

sub c_vote {
  my ($client,$k) = @_;
  # prout(" *> Vote [$k->{round}] $client->{host}:$client->{port} ".substr($k->{transhash},0,20));
  if (!defined $k->{round} || ($k->{round} =~ /[^0-9]/)) { killclient($client,"Illegal voting round") }
  if (!defined $k->{transhash} || (($k->{transhash} !~ /^[0-9A-F]{64}$/) && ($k->{transhash} !~ /^[0-9A-F]{128}$/))) { killclient($client,"Illegal transhash in voting") }
  if (!$VOTING && ($k->{round} == 0)) {
    votesuggest($k->{transhash})
  }
  if (!$VOTE->{responses}[$k->{round}]) { 
    $VOTE->{responses}[$k->{round}] = {};
    $VOTE->{received}[$k->{round}] = 0;
  }
  $VOTE->{responses}[$k->{round}]{$client->{mask}}={ 
    node => $client, round => $k->{round}, transhash => $k->{transhash}, ledgerlen => $k->{ledgerlen},
    tcum => $k->{tcum}, illegal => $k->{illegal}, consensus => $k->{consensus} 
  };
  $VOTE->{received}[$k->{round}]++;
  # prout(" *> Vote Noted [$k->{round}] $client->{host}:$client->{port} ".substr($k->{transhash},0,20))
}

############## Faults made by others ########################

sub fault {
  my ($client) = @_;
  $client->{fcc}{faults}++;
  if ($client->{fcc}{faults}>$MAXFAULTS) {
    outjson($client,{ command => 'error', error=>'Too many errors' });
    killclient($client)
  }
}

############### Transactions @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

sub newtrans {
  my ($client,$transid,$wallet,$pubkey,$sign,$numout) = @_;
  my $mask=$client->{mask}; my $ctm=time+$FCCTIME;
  $TRESLIST->{$mask}={} if !defined $TRESLIST->{$mask};
  $TRESLIST->{$mask}{$transid} = {
    transid => $transid, # needed when copied to translist
    wallet => $wallet,
    pubkey => $pubkey,
    signdata => $sign,
    fcctime => $ctm,
    numout => $numout
  };
  if ($client->{sess}) {
    $SESSLIST->{$client->{sess}} = {} if !$SESSLIST->{$client->{sess}};
    $SESSLIST->{$client->{sess}}{$transid} = {
      mask => $mask,
      transaction => $TRESLIST->{$mask}{$transid}
    };
    prout "New Session $client->{sess} Transaction $transid\n";
    delete $client->{sess};
  }
  return $ctm
}

sub transexists {
  my ($transhash) = @_;
  foreach my $trans (@$TRANSLIST) {
    if ($trans->{transhash} eq $transhash) { return 1 }
  }
  return 0
}

sub addtransaction {
  my ($client,$data,$skipvalidate) = @_;
  my $trans=splittransdata($data);
  if ($trans->{error}) {
    prout(" *! Transerror: $trans->{error}\n");
    fault($client); return $trans
  }
  $TRANSDISTDONE->{$client->{mask}.$trans->{transhash}}=1;
  if ($TRANSLIST->{$trans->{transhash}}) { $trans->{nocheck}=1; return $trans } # already present
  if ($TRANSLISTDONE->{$trans->{transhash}}) { $trans->{nocheck}=1; return $trans } # already done
  validateblock($trans);
  if ($trans->{error}) {
    prout(" *! Transerror: $trans->{error}\n");
    fault($client); return $trans
  }
  checkinblocks($trans);
  if ($trans->{error}) {
    prout(" *! Transerror: $trans->{error}\n");
    fault($client); return $trans
  }
  $trans->{status}=0;
  if ($client->{fcc}{function} eq 'leaf') {
    $trans->{client}=$client
  }
  $trans->{tobs}=1;
  #prout(" ++ Transaction $trans->{transhash} (fee = $trans->{fee})\n");
  $TRANSLIST->{$trans->{transhash}}=$trans;
  if ($TRANSCATCHUP->{$trans->{transhash}}) {
    # if not deleted, used as error-marker in voting round 1
    delete $TRANSCATCHUP->{$trans->{transhash}};
  } else {
    push @$TRANSDISTLIST,$data;
  }
  return $trans
}

sub checkinblocks {
  my ($trans) = @_;
  my %cib=(); foreach my $ib (@{$trans->{inblocks}}) { $cib{$ib}=1 }
  foreach my $t (keys %$TRANSLIST) {
    if ($TRANSLIST->{$t}{pubkey} && ($TRANSLIST->{$t}{pubkey} eq $trans->{pubkey})) {
      foreach my $ib (@{$TRANSLIST->{$t}{inblocks}}) {
        if ($cib{$ib}) {
          $trans->{error}="Double spending transaction flood detected";
          # We are under transaction-flood attack (like Verge was brought down april 2018)
          $BLOCKLIST->{$trans->{pubkey}}=time; return
        }
      }
    }
  }  
}

sub validateblock {
  my ($trans) = @_;
  if (!$trans->{amount}) { $trans->{error}="No amount given in transaction"; return }
  if ($#{$trans->{inblocks}}<0) { $trans->{error}="no inblocks"; return }
  if ($#{$trans->{outblocks}}<0) { $trans->{error}="No outblocks"; return }
  for (my $b=0; $b<=$#{$trans->{outblocks}}; $b++) {
    my $out=$trans->{outblocks}[$b];
    if (!validwallet($out->{wallet})) { $trans->{error}="Invalid wallet: $out->{wallet}"; return }
    if (($b<$#{$trans->{outblocks}}) || ($#{$trans->{outblocks}} == 0)) {
      if (!$out->{amount}) { $trans->{error}="No amount in send-block"; return }
      if ($out->{fee} < $MINIMUMFEE) { $trans->{error}="Illegal fee in send-block"; return }
    }
  }
  my ($inblocks,$change,$maxamount)=collectspendblocks($trans->{wallet},$trans->{amount}+$trans->{fee},transinblocks($trans->{wallet}));
  my $iblist=inblocklist($inblocks);
  if ($#{$iblist} != $#{$trans->{inblocks}}) { $trans->{error}="Not the same number of inblocks"; return }
  for (my $i=0;$i<=$#{$iblist};$i++) {
    if ($iblist->[$i] ne $trans->{inblocks}[$i]) { $trans->{error}="Not the same sequence of inblocks"; return }
  }
  if (!Crypt::Ed25519::verify($trans->{sign},hexoct($trans->{pubkey}),hexoct($trans->{signature}))) { $trans->{error}="Ed25519 Signature failed to verify"; return }
}

sub splittransdata {
  my ($data) = @_;
  if (!$data) { return { error => 'no data in transaction data' } }
  if ((length($data)<416) || ($data =~ /[^0-9A-F]/)) { return { error => 'illegal transaction data' } }
  my $info = {
   error => 0,
   fcctime => hexdec(substr($data,0,8)),
   pubkey => substr($data,8,64),
   numout => hexdec(substr($data,72,2)),
   signature => substr($data,-192,128),
   transhash => substr($data,-64),
   sign => substr($data,74,-192),
   inblocks => [],
   outblocks => []
  };
  my $epk=('0'x64); if ($epk eq $info->{pubkey}) { return { error => "Zero pubkey, h4x0rz" } }
  $info->{wallet}=createwalletaddress($info->{pubkey});
  my $blen=length($info->{sign}); my $outpos=$blen-$info->{numout}*88;
  if ($outpos % 64 != 0) { return { error => "outpos ($outpos) not divisable by 64" } }
  if ($outpos <= 0) { return { error => "Money does not come from nothing" } }
  if (($outpos>>6) > 255) { return { error => "Too many data needed to make this transaction. Please split up into smaller amounts" } }
  my $bl=0; my $numin=$outpos >> 6; my $pi=0;
  while ($bl<$numin) {
    push @{$info->{inblocks}},substr($info->{sign},$pi,64);
    $pi+=64; $bl++
  }
  my $amount=0; my $fee=0; $bl=0;
  while ($bl<$info->{numout}) {
    my $a=hexdec(substr($info->{sign},$pi+68,16));
    my $f=hexdec(substr($info->{sign},$pi+84,4));
    if ($bl<$info->{numout}-1) {
      $amount+=$a; $fee+=doggyfee($a,$f)
    }
    push @{$info->{outblocks}},{
      type => 'out',
      wallet => substr($info->{sign},$pi,68),
      amount => $a,
      fee => $f
    };
    $pi+=88; $bl++
  }
  $info->{amount}=$amount;
  $info->{fee}=$fee;
  $info->{data}=$data;
  return $info
}

sub transinblocks {
  my ($wallet) = @_;
  my $blocks=[];
  foreach my $trans (keys %$TRANSLIST) {
    if ($TRANSLIST->{$trans}{wallet} && ($TRANSLIST->{$trans}{wallet} eq $wallet)) {
      push @$blocks,@{$TRANSLIST->{$trans}{inblocks}}
    }
  }
  return $blocks
}

############### Voting System ###################################

sub votesuggest {
  my ($activate) = @_;
  
  if ($VOTING) { return }
#  if ($ADDCOINBASE) { return }

  # auto garbage collection
  $TRANSCATCHUP={};

  my $trans; my $coinbase=0;
  if ($#{$COINBASELIST}>=0) {
    $coinbase=1; $trans=$COINBASELIST->[0]{signature};
    $TRANSLIST->{$trans}={ coinbase => 1, transhash => $trans }
  } elsif (!$activate || (length($activate) == 64)) {
    my @tlist=sort { ($TRANSLIST->{$b}{fee} <=> $TRANSLIST->{$a}{fee}) || ($TRANSLIST->{$a}{fcctime} <=> $TRANSLIST->{$b}{fcctime}) } keys %$TRANSLIST;
    foreach my $t (@tlist) {
      if (!$TRANSLIST->{$t}{tobs} && !$TRANSLISTDONE->{$t}) {
        $trans=$t; last
      }
    }
  }

  my $tr=$trans || $activate;
  if (!$tr) { return }
#  prout("********** Votesuggest = $tr\n");

  my $nodelist=corelist();
  my $tm=gettimeofday();
  if ($trans) {
    # we found a transaction to suggest to the core (first voting round)
    $VOTE={
      round => 0,
      transhash => $trans,
      coinbase => $coinbase,
      responses => [],
      total => 1+$#{$nodelist},
      received => [],
      consensus => 0,
      start => $tm,
      illegal => 0
    };
    $VOTE->{received}[0]=0;
    $VOTE->{responses}[0]={};
    if ($#{$nodelist}<0) {
      # we are the only node! God save the Mohacans
      transtoledger($TRANSLIST->{$trans})
    } else {
      $VOTING=1;
      outcorejson({ command => 'vote', transhash => $trans, round => 0, ledgerlen => $LEDGERLEN, tcum => $LASTBLOCK->{tcum}, coinbase => $TRANSLIST->{$trans}{coinbase} })
    }
  } elsif ($activate) {
    if (!$TRANSLISTDONE->{$activate}) {
      # TRANSLIST seems to be empty, but they are voting on something.. get it (in voting round 0) and vote along!
      my $tm=gettimeofday();
      $VOTE={
        round => 0,
        transhash => $activate,
        coinbase => $coinbase,
        responses => [],
        total => 1+$#{$nodelist},
        received => [],
        consensus => 0,
        start => $tm,
        illegal => 0
      };
      $VOTE->{received}[0]=0;
      $VOTE->{responses}[0]={};
      if ($#{$nodelist}<0) {
        # we are the only node! God save the Mohacans
        transtoledger($TRANSLIST->{$trans})
      } else {
        $VOTING=1;
        my $ot={ 
          command => 'vote',
          transhash => $activate,
          round => 0,
          ledgerlen => $LEDGERLEN,
          tcum => $LASTBLOCK->{tcum},
          coinbase => $coinbase,
          dbhash => getdbhash()
        };
        outcorejson($ot)
      }
    }
  }
  if ($VOTING) { prout(" *> Voting system activated") } else { prout(" *> Voting suggestion rejected") }
  $AVT=0
}

sub analysevotes {

  if (!$VOTING) { return }

  my $action=0; my $tm=gettimeofday();
  if (!$VOTE->{received}[$VOTE->{round}]) {
    $VOTE->{received}[$VOTE->{round}]=0;
    $VOTE->{responses}[$VOTE->{round}]={}
  }
  my $dtm=int (100*($tm-$VOTE->{start}))/100; 
  if ($dtm-$AVT>1) {
    my $rec=$VOTE->{received}[$VOTE->{round}];
    prout(" *> VOTE [$VOTE->{round}] Received = $rec/$VOTE->{total} Time = $dtm");
    $AVT=$dtm
  }
  if (($VOTE->{received}[$VOTE->{round}] >= $VOTE->{total}) || ($tm-$VOTE->{start} >= 3)) {
    my @rplist=();
    foreach my $mask (keys %{$VOTE->{responses}[$VOTE->{round}]}) {
      my $rp=$VOTE->{responses}[$VOTE->{round}]{$mask}; push @rplist,$rp
    }
    my $nrp = 1 + $#rplist;
    # prout(" *> Voting: Received = $VOTE->{received}[$VOTE->{round}] NRP = $nrp\n");
    if ($#rplist < 0) {
      # we are stalled ..
      if ($VOTE->{total}) {
        killserver(" !*! No votes received ($VOTE->{total} expected) !*!"); exit
      } else {
        # we're the only node left in the core (the other one just quit)
        transtoledger($TRANSLIST->{$VOTE->{transhash}})
      }
      return
    }

  ######### Round 0 - Checking ledger sync & catching missing and illegal transactions ################

    if ($VOTE->{round} == 0) {
      # syncing transaction round, necessary for checking illegal transactions (distributed flood attack)!
      # search largest ledgerlength (if garbage, kill node afterwards)
      my @sortll = sort { $b->{ledgerlen} <=> $a->{ledgerlen} } @rplist;
      my $llwanted = $sortll[0]{ledgerlen};
      if ($llwanted > $LEDGERLEN) {
        # Running behind
        print("\r * Running ".($llwanted-$LEDGERLEN)." bytes behind, catching up            \n");
        killserver();
        return
      } elsif ($llwanted == $LEDGERLEN) {
        my $lch={};
        foreach my $rp (@rplist) {
          if ($rp->{tcum}) {
            if (!defined $lch->{$rp->{tcum}}) {
              $lch->{$rp->{tcum}}=1
            } else {
              $lch->{$rp->{tcum}}++
            }
          }
        }
        my @sch = sort { $lch->{$b} <=> $lch->{$a} } keys %$lch;
        if (($#sch>=0) && ($sch[0] ne $LASTBLOCK->{tcum})) {
          # Desynced !!!
          killserver("Unfortunately the ledger is desynced. Maybe the ledger should be deleted. Please restart the node."); exit
        } else {
          my $dbl={};
          foreach my $rp (@rplist) {
            if ($rp->{dbhash}) {
              if (!defined $lch->{$rp->{dbhash}}) {
                $dbl->{$rp->{dbhash}}=1
              } else {
                $dbl->{$rp->{dbhash}}++
              }
            }
          }
          my @sdl = sort { $dbl->{$b} <=> $dbl->{$a} } keys %$dbl;
          if (($#sdl>=0) && ($sdl[0] ne getdbhash())) {
            # Desynced !!!
            killdb();
            killserver("Unfortunately the databases are desynced. The databases are being rebuild after you restart the node."); exit
          } else {
            my $list={};
            foreach my $rp (@rplist) {
              $list->{$rp->{transhash}}++
            }
            my @slist = sort { $list->{$b} <=> $list->{$a} } keys %$list;
            my $n=1+$#slist;
            $VOTE->{transhash}=$slist[0];
            if (!$TRANSLIST->{$slist[0]} && !$VOTE->{illegal}) {
              if (!$TRANSCATCHUP->{$slist[0]}) {
                if ($tm - $VOTE->{start} >= 1) {
                  $TRANSCATCHUP->{$slist[0]}=1;
                  my $sent=0;
                  foreach my $rp (@rplist) {
                    if ($rp->{transhash} eq $slist[0]) {
                      outjson($rp->{node},{ command => 'reqtrans', transhash => $rp->{transhash} });
                      $sent++; if ($sent==3) { last }
                    }
                  }
                }
              } elsif ($VOTE->{illegal} || ($tm - $VOTE->{start} >= 3)) {
                # could not fetch transaction or illegal
                delvote(0); return
              }
            } else {
              $VOTE->{round}=1;
              $VOTE->{total}=1+$#rplist;
              $VOTE->{start}=$tm;
              foreach my $rp (@rplist) {
                outjson($rp->{node},{ command => 'vote', round => 1, transhash => $VOTE->{transhash}, illegal => $VOTE->{illegal} })
              }
            }
          }
        }
      }
      return
    }

  ########### Voting rounds #################

    my $list={}; my $vtot=0; my $illcnt=0;
    foreach my $rp (@rplist) {
      if ($rp->{illegal}) { $illcnt++ }
      my $q=1; if ($rp->{consensus}) { $q=$rp->{consensus} }
      $list->{$rp->{transhash}}+=$q; $vtot+=$q;
    }
    my @slist = sort { $list->{$b} <=> $list->{$a} } keys %$list;
    if ($#slist == 0) {
      # 100% consensus
      if ($TRANSLIST->{$slist[0]}) {
        # vote must be legal or would not be in translist
        # this is the perfect state, a perfectly synced core, even if we don't agree, then never mind
        transtoledger($TRANSLIST->{$slist[0]})
      } else {
        if ($VOTE->{illegal}) {
          if (!$illcnt) {
            prout(" !*! The core approved a transaction we determined illegal !*!");
            killserver("Unexpected illegal transaction")
          }
          delvote(0)
        } else {
          # the core voted on a new unknown transaction!
          if (!$TRANSCATCHUP->{$slist[0]}) {
            if ($tm - $VOTE->{start} >= 1) {
              $TRANSCATCHUP->{$slist[0]}=1;
              my $sent=0;
              foreach my $rp (@rplist) {
                if ($rp->{transhash} eq $slist[0]) {
                  outjson($rp->{node},{ command => 'reqtrans', transhash => $rp->{transhash} });
                  $sent++; if ($sent==3) { last }
                }
              }
            }
          } elsif ($VOTE->{illegal} || ($tm - $VOTE->{start} >= 3)) {
            # could not fetch transaction or illegal
            delvote(0)
          }          
        }
      }
      return
    }
    # No consensus!
    $VOTE->{illegal}=0;
    $VOTE->{transhash}=$slist[0];
    if ($TRANSLIST->{$slist[0]}) {
      # switch vote to majority
      $VOTE->{consensus}=int (100 * $list->{$slist[0]} / $vtot);
      $VOTE->{round}++;
      $VOTE->{start}=$tm;
      foreach my $rp (@rplist) {
        outjson($rp->{node},{ command => 'vote', round => $VOTE->{round}, transhash => $VOTE->{transhash}, illegal => $VOTE->{illegal}, consensus => $VOTE->{consensus} })
      }
      return
    }
    if (!$TRANSCATCHUP->{$slist[0]}) {
      if ($tm - $VOTE->{start} >= 1) {
        $TRANSCATCHUP->{$slist[0]}=1;
        my $sent=0;
        foreach my $rp (@rplist) {
          if ($rp->{transhash} eq $slist[0]) {
            outjson($rp->{node},{ command => 'reqtrans', transhash => $rp->{transhash} });
            $sent++; if ($sent==3) { last }
          }
        }
      }
    } elsif ($VOTE->{illegal} || ($tm - $VOTE->{start} >= 3)) {
      # could not fetch transaction or illegal
      delvote(0)
    }          
  }
}

sub delvote {
  my ($success) = @_;
  if (length($VOTE->{transhash}) == 128) {
    for (my $i=0;$i<=$#{$COINBASELIST};$i++) {
      if ($COINBASELIST->[$i]{signature} eq $VOTE->{transhash}) {
        splice(@$COINBASELIST,$i,1); last
      }
    }
  } elsif (!$success) {
    my $trans=$TRANSLIST->{$VOTE->{transhash}};
    prout(" %! Transaction rejected\n");
    if ($trans->{client}) {
      outjson($trans->{client},{ command => 'processed', transhash => $trans->{transhash}, error => "Rejected by the core" })
    }
  }
  $TRANSLISTDONE->{$VOTE->{transhash}}=1;
  delete $TRANSLIST->{$VOTE->{transhash}};
  $VOTING=0;
  $VOTE = {}
}

sub addcoinbasetoledger {
  my ($transhash) = @_;
  for (my $i=0;$i<=$#{$COINBASELIST};$i++) {
    if ($COINBASELIST->[$i]{signature} eq $transhash) {
      my $cb=$COINBASELIST->[$i];
      if ($cb->{blockheight}) {
        prout(" % Feetransaction created\n");
        foreach my $out (@{$cb->{outblocks}}) {
          if ($out->{wallet} eq $WALLET->{wallet}) {
            prout("% We earned $COIN ".extdec($out->{amount}/100000000)." !\n")
          }
        }
        createfeetransaction($cb->{fcctime},$cb->{blockheight},$cb->{spare},$cb->{signature},$cb->{outblocks});
        return 1
      } else {
        prout(" % Coinbase $cb->{coincount} created\n");
        if ($cb->{outblocks}[1]{wallet} eq $WALLET->{wallet}) {
          prout(" % We've earned a bonus of $COIN ".extdec($MINEBONUS/100000000)." !\n")
        }
        createcoinbase($cb->{fcctime},$cb->{coincount},$cb->{signature},$cb->{outblocks});
        return 1
      }
    }
  }
  prout(" *> Coinbase not found");
  return 0
}

sub transtoledger {
  my ($trans) = @_;
  if (length($trans->{transhash}) == 128) {
    if (!addcoinbasetoledger($trans->{transhash})) {
      # still waiting for the FCC-server
      prout(" *> Failed to add coinbase to ledger");
      $ADDCOINBASE=$trans->{transhash}
    }
  } else {
    prout(" % Transaction created (fee = ".extdec($trans->{fee}/100000000).")\n");
    my $tid = createtransaction($trans->{fcctime},$trans->{pubkey},$trans->{signature},$trans->{inblocks},$trans->{outblocks});
    if ($trans->{client}) {
      my $rpl = { 
        command => 'processed', 
        transhash => $trans->{transhash}, 
        wallet => $trans->{wallet}, 
        amount => $trans->{amount}, 
        fee => $trans->{fee}, 
        tid => $tid 
      };
      $rpl->{id} = $trans->{id} if defined $trans->{id};
      outjson($trans->{client},$rpl)
    }
  }
  $LASTBLOCK=readlastblock(); $LEDGERLEN=$LASTBLOCK->{pos}+$LASTBLOCK->{next}+4;
  delvote(1)
}

sub portforwarding {
  if ($FCCRECONNECT->{sec} > 0) { return }
  if ($SERVER->{killed}) { return } # dirty
  my $CNT="FCC !!"; if ($COIN eq 'PTTP') { $CNT='PTTP !' }
  print <<EOT;

******************************************************************************
*                                                                            *
*     PORT FORWARDING                                                        *
*                                                                            *
*     Your node has not been properly initialised for Port-Forwarding.       *
*     This means that although you can reach a bi-directional connection     *
*     from within, nodes cannot reach you from without, establishing a       *
*     connection.                                                            *
*                                                                            *
*     To fix this you must enable port forwarding, just like in most         *
*     Internet-games, in your router.                                        *
*                                                                            *
*     The default port is  7050  which must point to your local IP,          *
*     e.g. 192.168.xxx.xxx or 10.0.0.x                                       *
*     To find this local IP you can use ipconfig or ifconfig                 *
*                                                                            *
*     If this is all too difficult for you, stick to your wallet !           *
*     You don't need a node to be able to use $CNT Only nerds do.          *
*                                                                            *
*     Good luck,                                                             *
*                                                                            *
*     Chaosje                                                                *
*                                                                            *
******************************************************************************

EOT
}

# I love it when a plan comes together :)

# EOF FCC/PTTP Node by Chaosje (C) 2018 Domero