diff --git a/artefacts/test_wallet b/artefacts/test_wallet index a65ffa1..7c0755f 100644 Binary files a/artefacts/test_wallet and b/artefacts/test_wallet differ diff --git a/protobuf/gossip.proto b/protobuf/gossip.proto index 3880cf0..fad0072 100644 --- a/protobuf/gossip.proto +++ b/protobuf/gossip.proto @@ -54,4 +54,5 @@ service GossipAPI { rpc Discover(ConnectionData) returns (ConnectedNodes) {} rpc GossipVrx(VrxMsgGossip) returns (google.protobuf.Empty) {} rpc GossipTrx(TrxMsgGossip) returns (google.protobuf.Empty) {} + rpc GetVertex(SignedHash) returns (Vertex) {} } diff --git a/src/accountant/accountant.go b/src/accountant/accountant.go index 8d6a6c4..e4ab5f2 100644 --- a/src/accountant/accountant.go +++ b/src/accountant/accountant.go @@ -55,6 +55,7 @@ var ( ErrTransferringfundsFailure = errors.New("transferring spice failure") ErrEntityNotfund = errors.New("entity not fund") ErrBreak = errors.New("just break") + ErrParentDoesNotExists = errors.New("parent doesn't exists") ) type signatureVerifier interface { @@ -556,7 +557,7 @@ func (ab *AccountingBook) addLeafMemorized(ctx context.Context, m memory) error ) return ErrLeafRejected } - return nil + return ErrParentDoesNotExists } existringLeaf, ok := item.(*Vertex) if !ok { @@ -1115,3 +1116,13 @@ func (ab *AccountingBook) ReadDAGTransactionsByAddress(ctx context.Context, addr func (ab *AccountingBook) Address() string { return ab.signer.Address() } + +// ReadVertex reads vertex by its hash from the vertex if exists or returns error otherwise. +func (ab *AccountingBook) ReadVertex(ctx context.Context, h [32]byte) (Vertex, error) { + v, err := ab.readVertex(h[:]) + if err != nil { + ab.log.Info(fmt.Sprintf("reading vertex hash %v failed, %s", h, err.Error())) + return Vertex{}, ErrVertexHashNotfund + } + return v, nil +} diff --git a/src/accountant/replier.go b/src/accountant/replier.go index 257e737..584913b 100644 --- a/src/accountant/replier.go +++ b/src/accountant/replier.go @@ -9,8 +9,8 @@ import ( ) const ( - maxArraySize = 250 - maxRepeats = 5 + maxArraySize = 500 + maxRepeats = 25 ) const longevity = time.Minute diff --git a/src/cmd/wallet/main.go b/src/cmd/wallet/main.go index 3b2dd25..459cdb7 100644 --- a/src/cmd/wallet/main.go +++ b/src/cmd/wallet/main.go @@ -16,6 +16,7 @@ import ( "github.com/bartossh/Computantis/src/walletmiddleware" "github.com/pterm/pterm" "github.com/urfave/cli/v2" + "golang.org/x/exp/rand" ) const ( @@ -25,8 +26,6 @@ const ( actionReadAddress ) -const pauseDuration = time.Second * 2 - const usage = `Wallet CLI tool allows to create a new Wallet or act on the local Wallet by using keys from different formats and transforming them between formats. Please use with the best security practices. GOBINARY is safer to move between machines as this file format is encrypted with AES key. Tool provides Spice and Contract transfer, reading balance, reading contracts, approving and rejecting contracts.` @@ -305,7 +304,7 @@ func runTransactionOps(cfg fileoperations.Config, nodeURL string) error { continue } spinnerInfo, _ := pterm.DefaultSpinner.Start("Sending transaction ...") - time.Sleep(pauseDuration) + time.Sleep(getPause()) if err := c.ProposeTransaction(ctx, receiver, subject, melange, []byte{}); err != nil { spinnerInfo.Stop() printError(fmt.Errorf("cannot propose transaction due to, %e", err)) @@ -315,7 +314,7 @@ func runTransactionOps(cfg fileoperations.Config, nodeURL string) error { printSuccess() case "Check balance": spinnerInfo, _ := pterm.DefaultSpinner.Start("Checking balance ...") - time.Sleep(pauseDuration) + time.Sleep(getPause()) melange, err := c.ReadBalance(ctx) if err != nil { spinnerInfo.Stop() @@ -332,7 +331,7 @@ func runTransactionOps(cfg fileoperations.Config, nodeURL string) error { printSuccess() case "Read Transactions": spinnerInfo, _ := pterm.DefaultSpinner.Start("Reading transactions ...") - time.Sleep(pauseDuration) + time.Sleep(getPause()) transactions, err := c.ReadDAGTransactions(ctx) if err != nil { spinnerInfo.Stop() @@ -453,3 +452,8 @@ func printWarning(warning string) { pterm.Warning.Printf(" %s\n", warning) pterm.Warning.Println("") } + +func getPause() time.Duration { + p := rand.Intn(5) + return time.Duration(p) * time.Second +} diff --git a/src/gossip/gossip.go b/src/gossip/gossip.go index 61f7262..8df1eb8 100644 --- a/src/gossip/gossip.go +++ b/src/gossip/gossip.go @@ -9,6 +9,7 @@ import ( "io" "net" "sync" + "sync/atomic" "time" "github.com/bartossh/Computantis/src/accountant" @@ -35,6 +36,7 @@ var ( ErrNilTrx = errors.New("transaction is nil") ErrNilTrxMsgGossip = errors.New("transaction gossip is nil") ErrNilSignedHash = errors.New("signed hash is nil") + ErrInvalidSignature = errors.New("invalid signature") ) const ( @@ -42,10 +44,11 @@ const ( ) const ( - vertexGossipChCapacity = 100 - trxGossipChCappacity = 100 - rejectHashChCapacity = 80 - totalRetries = 10 + vertexGossipChCapacity = 100 + trxGossipChCappacity = 100 + rejectHashChCapacity = 80 + totalRetries = 10 + maxVertexParentProcessingCount = 250 ) type nodeData struct { @@ -81,6 +84,7 @@ type accounter interface { StreamDAG(ctx context.Context) <-chan *accountant.Vertex LoadDag(cancelF context.CancelCauseFunc, cVrx <-chan *accountant.Vertex) DagLoaded() bool + ReadVertex(ctx context.Context, h [32]byte) (accountant.Vertex, error) } type piper interface { @@ -90,17 +94,18 @@ type piper interface { type gossiper struct { protobufcompiled.UnimplementedGossipAPIServer - accounter accounter - verifier signatureVerifier - signer accountant.Signer - log logger.Logger - trxCache providers.AwaitedTrxCacheProviderBalanceCacher - flash providers.FlashbackMemoryHashProviderAddressRemover - piper piper - nodes map[string]nodeData - url string - mux sync.RWMutex - timeout time.Duration + accounter accounter + verifier signatureVerifier + signer accountant.Signer + log logger.Logger + trxCache providers.AwaitedTrxCacheProviderBalanceCacher + flash providers.FlashbackMemoryHashProviderAddressRemover + piper piper + nodes map[string]nodeData + url string + mux sync.RWMutex + timeout time.Duration + processingParentCount atomic.Int32 } // RunGRPC runs the service application that exposes the GRPC API for gossip protocol. @@ -117,17 +122,18 @@ func RunGRPC(ctx context.Context, cfg Config, l logger.Logger, t time.Duration, defer cancel() g := gossiper{ - accounter: a, - verifier: v, - signer: s, - log: l, - trxCache: trxCache, - flash: flash, - piper: p, - nodes: make(map[string]nodeData), - url: cfg.URL, - mux: sync.RWMutex{}, - timeout: t, + accounter: a, + verifier: v, + signer: s, + log: l, + trxCache: trxCache, + flash: flash, + piper: p, + nodes: make(map[string]nodeData), + url: cfg.URL, + mux: sync.RWMutex{}, + timeout: t, + processingParentCount: atomic.Int32{}, } switch cfg.LoadDagURL { @@ -402,6 +408,19 @@ func (g *gossiper) GossipTrx(ctx context.Context, tg *protobufcompiled.TrxMsgGos return &emptypb.Empty{}, nil } +// GetVertex reads vertex from the accountant internal DAG by its hash if exists or returns error otherwise. +func (g *gossiper) GetVertex(ctx context.Context, in *protobufcompiled.SignedHash) (*protobufcompiled.Vertex, error) { + if err := g.verifier.Verify(in.Data, in.Signature, [32]byte(in.Hash), in.Address); err != nil { + g.log.Error(fmt.Sprintf("get vertex endpoint failed to verify signature of transaction [ %x ] for address: %s, %s", in.Hash, in.Address, err)) + return nil, ErrInvalidSignature + } + vrx, err := g.accounter.ReadVertex(ctx, [32]byte(in.Data)) + if err != nil { + return nil, err + } + return mapAccountantVertexToProtoVertex(&vrx), nil +} + func (g *gossiper) updateDag(ctx context.Context, url string) error { nd, err := connectToNode(url) if err != nil { @@ -611,12 +630,56 @@ func (g *gossiper) updateNodesConnectionsFromGensisNode(ctx context.Context, gen return nil } +func (g *gossiper) processLackingParent(ctx context.Context, h [32]byte) { + if g.processingParentCount.Load() == maxVertexParentProcessingCount { + return + } + g.processingParentCount.Add(1) + + digest, signature := g.signer.Sign(h[:]) + for _, nd := range g.nodes { + + vrx, err := nd.client.GetVertex(ctx, &protobufcompiled.SignedHash{ + Address: g.signer.Address(), + Data: h[:], + Hash: digest[:], + Signature: signature, + }) + if err != nil || vrx == nil { + continue + } + v := mapProtoVertexToAccountantVertex(vrx) + if err := g.accounter.AddLeaf(ctx, &v); err != nil { + if errors.Is(err, accountant.ErrParentDoesNotExists) { + parentHases := [][32]byte{v.LeftParentHash} + if v.LeftParentHash != v.RightParentHash { + parentHases = append(parentHases, v.RightParentHash) + } + for _, parentHash := range parentHases { + go g.processLackingParent(ctx, parentHash) + } + } + } + break + } + g.processingParentCount.Add(-1) +} + func (g *gossiper) sendToAccountant(ctx context.Context, vg *protobufcompiled.Vertex) error { if vg.Transaction == nil { return ErrNilTrx } v := mapProtoVertexToAccountantVertex(vg) if err := g.accounter.AddLeaf(ctx, &v); err != nil { + if errors.Is(err, accountant.ErrParentDoesNotExists) { + parentHases := [][32]byte{v.LeftParentHash} + if v.LeftParentHash != v.RightParentHash { + parentHases = append(parentHases, v.RightParentHash) + } + for _, parentHash := range parentHases { + go g.processLackingParent(ctx, parentHash) + } + } return err } return nil diff --git a/src/protobufcompiled/gossip.pb.go b/src/protobufcompiled/gossip.pb.go index 704b83e..2a357a0 100644 --- a/src/protobufcompiled/gossip.pb.go +++ b/src/protobufcompiled/gossip.pb.go @@ -497,7 +497,7 @@ var file_gossip_proto_rawDesc = []byte{ 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x61, 0x6e, 0x74, 0x69, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x32, 0x91, 0x03, 0x0a, 0x09, 0x47, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x41, 0x50, 0x49, + 0x6e, 0x73, 0x32, 0xce, 0x03, 0x0a, 0x09, 0x47, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x41, 0x50, 0x49, 0x12, 0x39, 0x0a, 0x05, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x61, 0x6e, 0x74, 0x69, 0x73, 0x2e, @@ -522,11 +522,15 @@ var file_gossip_proto_rawDesc = []byte{ 0x78, 0x12, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x61, 0x6e, 0x74, 0x69, 0x73, 0x2e, 0x54, 0x72, 0x78, 0x4d, 0x73, 0x67, 0x47, 0x6f, 0x73, 0x73, 0x69, 0x70, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x61, 0x72, 0x74, 0x6f, 0x73, 0x73, 0x68, 0x2f, 0x43, 0x6f, - 0x6d, 0x70, 0x75, 0x74, 0x61, 0x6e, 0x74, 0x69, 0x73, 0x2f, 0x73, 0x72, 0x63, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, + 0x74, 0x65, 0x78, 0x12, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x61, 0x6e, 0x74, 0x69, + 0x73, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x13, 0x2e, 0x63, + 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x61, 0x6e, 0x74, 0x69, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x74, 0x65, + 0x78, 0x22, 0x00, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x62, 0x61, 0x72, 0x74, 0x6f, 0x73, 0x73, 0x68, 0x2f, 0x43, 0x6f, 0x6d, 0x70, 0x75, + 0x74, 0x61, 0x6e, 0x74, 0x69, 0x73, 0x2f, 0x73, 0x72, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -551,7 +555,8 @@ var file_gossip_proto_goTypes = []interface{}{ (*ConnectedNodes)(nil), // 5: computantis.ConnectedNodes (*Transaction)(nil), // 6: computantis.Transaction (*emptypb.Empty)(nil), // 7: google.protobuf.Empty - (*AliveData)(nil), // 8: computantis.AliveData + (*SignedHash)(nil), // 8: computantis.SignedHash + (*AliveData)(nil), // 9: computantis.AliveData } var file_gossip_proto_depIdxs = []int32{ 6, // 0: computantis.Vertex.transaction:type_name -> computantis.Transaction @@ -566,14 +571,16 @@ var file_gossip_proto_depIdxs = []int32{ 4, // 9: computantis.GossipAPI.Discover:input_type -> computantis.ConnectionData 2, // 10: computantis.GossipAPI.GossipVrx:input_type -> computantis.VrxMsgGossip 3, // 11: computantis.GossipAPI.GossipTrx:input_type -> computantis.TrxMsgGossip - 8, // 12: computantis.GossipAPI.Alive:output_type -> computantis.AliveData - 0, // 13: computantis.GossipAPI.LoadDag:output_type -> computantis.Vertex - 7, // 14: computantis.GossipAPI.Announce:output_type -> google.protobuf.Empty - 5, // 15: computantis.GossipAPI.Discover:output_type -> computantis.ConnectedNodes - 7, // 16: computantis.GossipAPI.GossipVrx:output_type -> google.protobuf.Empty - 7, // 17: computantis.GossipAPI.GossipTrx:output_type -> google.protobuf.Empty - 12, // [12:18] is the sub-list for method output_type - 6, // [6:12] is the sub-list for method input_type + 8, // 12: computantis.GossipAPI.GetVertex:input_type -> computantis.SignedHash + 9, // 13: computantis.GossipAPI.Alive:output_type -> computantis.AliveData + 0, // 14: computantis.GossipAPI.LoadDag:output_type -> computantis.Vertex + 7, // 15: computantis.GossipAPI.Announce:output_type -> google.protobuf.Empty + 5, // 16: computantis.GossipAPI.Discover:output_type -> computantis.ConnectedNodes + 7, // 17: computantis.GossipAPI.GossipVrx:output_type -> google.protobuf.Empty + 7, // 18: computantis.GossipAPI.GossipTrx:output_type -> google.protobuf.Empty + 0, // 19: computantis.GossipAPI.GetVertex:output_type -> computantis.Vertex + 13, // [13:20] is the sub-list for method output_type + 6, // [6:13] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name 6, // [6:6] is the sub-list for extension extendee 0, // [0:6] is the sub-list for field type_name diff --git a/src/protobufcompiled/gossip_grpc.pb.go b/src/protobufcompiled/gossip_grpc.pb.go index 413a590..5c37c7b 100644 --- a/src/protobufcompiled/gossip_grpc.pb.go +++ b/src/protobufcompiled/gossip_grpc.pb.go @@ -29,6 +29,7 @@ type GossipAPIClient interface { Discover(ctx context.Context, in *ConnectionData, opts ...grpc.CallOption) (*ConnectedNodes, error) GossipVrx(ctx context.Context, in *VrxMsgGossip, opts ...grpc.CallOption) (*emptypb.Empty, error) GossipTrx(ctx context.Context, in *TrxMsgGossip, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetVertex(ctx context.Context, in *SignedHash, opts ...grpc.CallOption) (*Vertex, error) } type gossipAPIClient struct { @@ -116,6 +117,15 @@ func (c *gossipAPIClient) GossipTrx(ctx context.Context, in *TrxMsgGossip, opts return out, nil } +func (c *gossipAPIClient) GetVertex(ctx context.Context, in *SignedHash, opts ...grpc.CallOption) (*Vertex, error) { + out := new(Vertex) + err := c.cc.Invoke(ctx, "/computantis.GossipAPI/GetVertex", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // GossipAPIServer is the server API for GossipAPI service. // All implementations must embed UnimplementedGossipAPIServer // for forward compatibility @@ -126,6 +136,7 @@ type GossipAPIServer interface { Discover(context.Context, *ConnectionData) (*ConnectedNodes, error) GossipVrx(context.Context, *VrxMsgGossip) (*emptypb.Empty, error) GossipTrx(context.Context, *TrxMsgGossip) (*emptypb.Empty, error) + GetVertex(context.Context, *SignedHash) (*Vertex, error) mustEmbedUnimplementedGossipAPIServer() } @@ -151,6 +162,9 @@ func (UnimplementedGossipAPIServer) GossipVrx(context.Context, *VrxMsgGossip) (* func (UnimplementedGossipAPIServer) GossipTrx(context.Context, *TrxMsgGossip) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method GossipTrx not implemented") } +func (UnimplementedGossipAPIServer) GetVertex(context.Context, *SignedHash) (*Vertex, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetVertex not implemented") +} func (UnimplementedGossipAPIServer) mustEmbedUnimplementedGossipAPIServer() {} // UnsafeGossipAPIServer may be embedded to opt out of forward compatibility for this service. @@ -275,6 +289,24 @@ func _GossipAPI_GossipTrx_Handler(srv interface{}, ctx context.Context, dec func return interceptor(ctx, in, info, handler) } +func _GossipAPI_GetVertex_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignedHash) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GossipAPIServer).GetVertex(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/computantis.GossipAPI/GetVertex", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GossipAPIServer).GetVertex(ctx, req.(*SignedHash)) + } + return interceptor(ctx, in, info, handler) +} + // GossipAPI_ServiceDesc is the grpc.ServiceDesc for GossipAPI service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -302,6 +334,10 @@ var GossipAPI_ServiceDesc = grpc.ServiceDesc{ MethodName: "GossipTrx", Handler: _GossipAPI_GossipTrx_Handler, }, + { + MethodName: "GetVertex", + Handler: _GossipAPI_GetVertex_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/src/spice/spice.go b/src/spice/spice.go index ecfe4cd..0e25866 100644 --- a/src/spice/spice.go +++ b/src/spice/spice.go @@ -51,16 +51,6 @@ func convertFloatToInt(d float64) (int, int) { return intPart, fractionalPart } -func GetSientific(v uint64) string { - var zeros int - for v%10 == 0 { - zeros++ - v /= 10 - } - - return fmt.Sprintf("%v*10^%v", v, zeros) -} - // Melange is an asset that is digitally transferable between two wallets. type Melange struct { Currency uint64 `yaml:"currency" msgpack:"currency"` diff --git a/src/spice/spice_test.go b/src/spice/spice_test.go index 287055a..9e94313 100644 --- a/src/spice/spice_test.go +++ b/src/spice/spice_test.go @@ -159,28 +159,6 @@ func BenchmarkMelangeTransfer(b *testing.B) { } } -func TestPrintGetScientific(t *testing.T) { - cases := []uint64{ - 10, - 100, - 1000, - 10000, - 100000, - 1000000, - 1100000, - 1110000, - 1111000, - 1111100, - 1111110, - } - for _, c := range cases { - t.Run(fmt.Sprintf("get scientific test case %v", c), func(t *testing.T) { - s := GetSientific(c) - fmt.Printf("%s\n", s) - }) - } -} - func TestFromFloat(t *testing.T) { cases := []struct { expected string