diff --git a/asset-transfer-private-data/application-gateway-go/connect.go b/asset-transfer-private-data/application-gateway-go/connect.go index 3bd768634..334ffd9fb 100644 --- a/asset-transfer-private-data/application-gateway-go/connect.go +++ b/asset-transfer-private-data/application-gateway-go/connect.go @@ -1,5 +1,5 @@ /* -Copyright 2022 IBM All Rights Reserved. +Copyright 2024 IBM All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ @@ -77,8 +77,8 @@ func newIdentity(certDirectoryPath, mspId string) *identity.X509Identity { } // newSign creates a function that generates a digital signature from a message digest using a private key. -func newSign(keyDirectoryPash string) identity.Sign { - privateKeyPEM, err := readFirstFile(keyDirectoryPash) +func newSign(keyDirectoryPath string) identity.Sign { + privateKeyPEM, err := readFirstFile(keyDirectoryPath) if err != nil { panic(fmt.Errorf("failed to read private key file: %w", err)) } diff --git a/off_chain_data/README.md b/off_chain_data/README.md index 3b931492a..a78b84b0e 100644 --- a/off_chain_data/README.md +++ b/off_chain_data/README.md @@ -28,7 +28,7 @@ The client application provides several "commands" that can be invoked using the To keep the sample code concise, the **listen** command writes ledger updates to an output file named `store.log` in the current working directory (which for the Java sample is the `application-java/app` directory). A real implementation could write ledger updates directly to an off-chain data store of choice. You can inspect the information captured in this file as you run the sample. -Note that the **listen** command is is restartable and will resume event listening after the last successfully processed block / transaction. This is achieved using a checkpointer to persist the current listening position. Checkpoint state is persisted to a file named `checkpoint.json` in the current working directory. If no checkpoint state is present, event listening begins from the start of the ledger (block number zero). +Note that the **listen** command is restartable and will resume event listening after the last successfully processed block / transaction. This is achieved using a checkpointer to persist the current listening position. Checkpoint state is persisted to a file named `checkpoint.json` in the current working directory. If no checkpoint state is present, event listening begins from the start of the ledger (block number zero). ### Smart Contract @@ -112,4 +112,4 @@ When you are finished, you can bring down the test network (from the `test-netwo ``` ./network.sh down -``` \ No newline at end of file +``` diff --git a/off_chain_data/application-go/app.go b/off_chain_data/application-go/app.go new file mode 100644 index 000000000..fd8fd0796 --- /dev/null +++ b/off_chain_data/application-go/app.go @@ -0,0 +1,62 @@ +/* + * Copyright 2024 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "errors" + "fmt" + "os" + "strings" + + "google.golang.org/grpc" +) + +var allCommands = map[string]func(clientConnection *grpc.ClientConn){ + "getAllAssets": getAllAssets, + "transact": transact, + "listen": listen, +} + +func main() { + commands := os.Args[1:] + if len(commands) == 0 { + printUsage() + panic(errors.New("missing command")) + } + + for _, name := range commands { + if _, exists := allCommands[name]; !exists { + printUsage() + panic(fmt.Errorf("unknown command: %s", name)) + } + fmt.Printf("command: %s\n", name) + } + + client := newGrpcConnection() + defer client.Close() + + for _, name := range commands { + command := allCommands[name] + command(client) + } +} + +func printUsage() { + fmt.Println("Arguments: [ ...]") + fmt.Printf("Available commands: %v\n", availableCommands()) +} + +func availableCommands() string { + result := make([]string, len(allCommands)) + i := 0 + for command := range allCommands { + result[i] = command + i++ + } + + return strings.Join(result, ", ") +} diff --git a/off_chain_data/application-go/blockParser.go b/off_chain_data/application-go/blockParser.go new file mode 100644 index 000000000..102b50fa3 --- /dev/null +++ b/off_chain_data/application-go/blockParser.go @@ -0,0 +1,259 @@ +package main + +import ( + "github.com/hyperledger/fabric-gateway/pkg/identity" + "github.com/hyperledger/fabric-protos-go-apiv2/common" + "github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset" + "github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset" + "github.com/hyperledger/fabric-protos-go-apiv2/peer" + "google.golang.org/protobuf/proto" +) + +type Block interface { + GetNumber() uint64 + GetTransactions() []Transaction + ToProto() *common.Block +} + +type Transaction interface { + getChannelHeader() common.ChannelHeader + getCreator() identity.Identity + getValidationCode() uint64 + IsValid() bool + GetNamespaceReadWriteSets() []NamespaceReadWriteSet + ToProto() common.Payload +} + +type ParsedBlock struct { + block *common.Block + validationCodes []byte + transactions []Transaction +} + +func NewParsedBlock(block *common.Block) Block { + validationCodes := getTransactionValidationCodes(block) + + return &ParsedBlock{block, validationCodes, nil} +} + +func (pb *ParsedBlock) GetNumber() uint64 { + header := assertDefined(pb.block.GetHeader(), "missing block header") + return header.GetNumber() +} + +// TODO: needs cache, getPayloads, parsePayload +func (pb *ParsedBlock) GetTransactions() []Transaction { + return nil +} + +func (pb *ParsedBlock) ToProto() *common.Block { + return nil +} + +type EndorserTransaction interface { + GetReadWriteSets() []ReadWriteSet + ToProto() *peer.Transaction +} + +type ParsedEndorserTransaction struct { + transaction *peer.Transaction +} + +// TODO add cache +func (p *ParsedEndorserTransaction) GetReadWriteSets() []ReadWriteSet { + chaincodeActionPayloads := p.getChaincodeActionPayloads() + + chaincodeEndorsedActions := p.getChaincodeEndorsedActions(chaincodeActionPayloads) + + proposalResponsePayloads := p.getProposalResponsePayloads(chaincodeEndorsedActions) + + chaincodeActions := p.getChaincodeActions(proposalResponsePayloads) + + txReadWriteSets := p.getTxReadWriteSets(chaincodeActions) + + parsedReadWriteSets := p.parseReadWriteSets(txReadWriteSets) + + return parsedReadWriteSets +} + +func (*ParsedEndorserTransaction) parseReadWriteSets(txReadWriteSets []*rwset.TxReadWriteSet) []ReadWriteSet { + parsedReadWriteSets := []ReadWriteSet{} + for _, txReadWriteSet := range txReadWriteSets { + parsedReadWriteSet := &ParsedReadWriteSet{txReadWriteSet} + parsedReadWriteSets = append(parsedReadWriteSets, parsedReadWriteSet) + } + return parsedReadWriteSets +} + +func (*ParsedEndorserTransaction) getTxReadWriteSets(chaincodeActions []*peer.ChaincodeAction) []*rwset.TxReadWriteSet { + txReadWriteSets := []*rwset.TxReadWriteSet{} + for _, chaincodeAction := range chaincodeActions { + txReadWriteSet := &rwset.TxReadWriteSet{} + if err := proto.Unmarshal(chaincodeAction.GetResults(), txReadWriteSet); err != nil { + continue + } + txReadWriteSets = append(txReadWriteSets, txReadWriteSet) + } + return txReadWriteSets +} + +func (*ParsedEndorserTransaction) getChaincodeActions(proposalResponsePayloads []*peer.ProposalResponsePayload) []*peer.ChaincodeAction { + chaincodeActions := []*peer.ChaincodeAction{} + for _, proposalResponsePayload := range proposalResponsePayloads { + chaincodeAction := &peer.ChaincodeAction{} + if err := proto.Unmarshal(proposalResponsePayload.GetExtension(), chaincodeAction); err != nil { + continue + } + chaincodeActions = append(chaincodeActions, chaincodeAction) + } + return chaincodeActions +} + +func (*ParsedEndorserTransaction) getProposalResponsePayloads(chaincodeEndorsedActions []*peer.ChaincodeEndorsedAction) []*peer.ProposalResponsePayload { + proposalResponsePayloads := []*peer.ProposalResponsePayload{} + for _, endorsedAction := range chaincodeEndorsedActions { + proposalResponsePayload := &peer.ProposalResponsePayload{} + if err := proto.Unmarshal(endorsedAction.GetProposalResponsePayload(), proposalResponsePayload); err != nil { + continue + } + proposalResponsePayloads = append(proposalResponsePayloads, proposalResponsePayload) + } + return proposalResponsePayloads +} + +func (*ParsedEndorserTransaction) getChaincodeEndorsedActions(chaincodeActionPayloads []*peer.ChaincodeActionPayload) []*peer.ChaincodeEndorsedAction { + chaincodeEndorsedActions := []*peer.ChaincodeEndorsedAction{} + for _, payload := range chaincodeActionPayloads { + chaincodeEndorsedActions = append(chaincodeEndorsedActions, assertDefined(payload.GetAction(), "missing chaincode endorsed action")) + } + return chaincodeEndorsedActions +} + +func (p *ParsedEndorserTransaction) getChaincodeActionPayloads() []*peer.ChaincodeActionPayload { + chaincodeActionPayloads := []*peer.ChaincodeActionPayload{} + for _, transactionAction := range p.transaction.GetActions() { + chaincodeActionPayload := &peer.ChaincodeActionPayload{} + if err := proto.Unmarshal(transactionAction.GetPayload(), chaincodeActionPayload); err != nil { + continue + } + + chaincodeActionPayloads = append(chaincodeActionPayloads, chaincodeActionPayload) + } + return chaincodeActionPayloads +} + +func (p *ParsedEndorserTransaction) ToProto() *peer.Transaction { + return p.transaction +} + +type ReadWriteSet interface { + GetNamespaceReadWriteSets() []NamespaceReadWriteSet + ToProto() *rwset.TxReadWriteSet +} + +type ParsedReadWriteSet struct { + readWriteSet *rwset.TxReadWriteSet +} + +func (p *ParsedReadWriteSet) GetNamespaceReadWriteSets() []NamespaceReadWriteSet { + nsReadWriteSets := make([]NamespaceReadWriteSet, 0) + for _, nsReadWriteSet := range p.readWriteSet.GetNsRwset() { + parsedNamespaceReadWriteSet := ParsedNamespaceReadWriteSet{nsReadWriteSet} + nsReadWriteSets = append(nsReadWriteSets, &parsedNamespaceReadWriteSet) + } + return nsReadWriteSets +} + +func (p *ParsedReadWriteSet) ToProto() *rwset.TxReadWriteSet { + return p.readWriteSet +} + +type NamespaceReadWriteSet interface { + GetNamespace() string + GetReadWriteSet() *kvrwset.KVRWSet + ToProto() *rwset.NsReadWriteSet +} + +type ParsedNamespaceReadWriteSet struct { + nsReadWriteSet *rwset.NsReadWriteSet +} + +func (p *ParsedNamespaceReadWriteSet) GetNamespace() string { + return p.nsReadWriteSet.GetNamespace() +} + +// TODO add cache +func (p *ParsedNamespaceReadWriteSet) GetReadWriteSet() *kvrwset.KVRWSet { + readWriteSet := kvrwset.KVRWSet{} + if err := proto.Unmarshal(p.nsReadWriteSet.GetRwset(), &readWriteSet); err != nil { + panic(err) + } + + return &readWriteSet +} + +func (p *ParsedNamespaceReadWriteSet) ToProto() *rwset.NsReadWriteSet { + return p.nsReadWriteSet +} + +func (pb *ParsedBlock) payloads() []*common.Payload { + var payloads []*common.Payload + + for _, envelopeBytes := range pb.block.GetData().GetData() { + envelope := &common.Envelope{} + if err := proto.Unmarshal(envelopeBytes, envelope); err != nil { + panic(err) + } + + payload := &common.Payload{} + if err := proto.Unmarshal(envelope.Payload, payload); err != nil { + panic(err) + } + + payloads = append(payloads, payload) + } + + return payloads +} + +// TODO not sure about this +func (pb *ParsedBlock) statusCode(txIndex int) peer.TxValidationCode { + blockMetadata := assertDefined( + pb.block.GetMetadata(), + "missing block metadata", + ) + + metadata := blockMetadata.GetMetadata() + if int(common.BlockMetadataIndex_TRANSACTIONS_FILTER) >= len(metadata) { + return peer.TxValidationCode_INVALID_OTHER_REASON + } + + statusCodes := metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER] + if txIndex >= len(statusCodes) { + return peer.TxValidationCode_INVALID_OTHER_REASON + } + + return peer.TxValidationCode(statusCodes[txIndex]) +} + +type Payload interface { + GetChannelHeader() common.ChannelHeader + GetEndorserTransaction() EndorserTransaction + GetSignatureHeader() common.SignatureHeader + GetTransactionValidationCode() uint64 + IsEndorserTransaction() bool + IsValid() bool + ToProto() common.Payload +} + +func getTransactionValidationCodes(block *common.Block) []byte { + metadata := assertDefined( + block.GetMetadata(), + "missing block metadata", + ) + + return assertDefined( + metadata.GetMetadata()[common.BlockMetadataIndex_TRANSACTIONS_FILTER], + "missing transaction validation code", + ) +} diff --git a/off_chain_data/application-go/connect.go b/off_chain_data/application-go/connect.go new file mode 100644 index 000000000..91a69676b --- /dev/null +++ b/off_chain_data/application-go/connect.go @@ -0,0 +1,142 @@ +/* + * Copyright 2024 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "crypto/x509" + "fmt" + "os" + "path" + "time" + + "github.com/hyperledger/fabric-gateway/pkg/client" + "github.com/hyperledger/fabric-gateway/pkg/hash" + "github.com/hyperledger/fabric-gateway/pkg/identity" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +const peerName = "peer0.org1.example.com" + +var ( + channelName = envOrDefault("CHANNEL_NAME", "mychannel") + chaincodeName = envOrDefault("CHAINCODE_NAME", "basic") + mspID = envOrDefault("MSP_ID", "Org1MSP") + + // Path to crypto materials. + cryptoPath = envOrDefault("CRYPTO_PATH", "../../test-network/organizations/peerOrganizations/org1.example.com") + + // Path to user private key directory. + keyDirectoryPath = envOrDefault("KEY_DIRECTORY_PATH", cryptoPath+"/users/User1@org1.example.com/msp/keystore") + + // Path to user certificate. + certPath = envOrDefault("CERT_PATH", cryptoPath+"/users/User1@org1.example.com/msp/signcerts/cert.pem") + + // Path to peer tls certificate. + tlsCertPath = envOrDefault("TLS_CERT_PATH", cryptoPath+"/peers/peer0.org1.example.com/tls/ca.crt") + + // Gateway peer endpoint. + peerEndpoint = envOrDefault("PEER_ENDPOINT", "dns:///localhost:7051") + + // Gateway peer SSL host name override. + peerHostAlias = envOrDefault("PEER_HOST_ALIAS", peerName) +) + +func envOrDefault(key, defaultValue string) string { + result := os.Getenv(key) + if result == "" { + return defaultValue + } + return result +} + +func newGrpcConnection() *grpc.ClientConn { + certificatePEM, err := os.ReadFile(tlsCertPath) + if err != nil { + panic(fmt.Errorf("failed to read TLS certificate file: %w", err)) + } + + certificate, err := identity.CertificateFromPEM(certificatePEM) + if err != nil { + panic(err) + } + + certPool := x509.NewCertPool() + certPool.AddCert(certificate) + transportCredentials := credentials.NewClientTLSFromCert(certPool, peerHostAlias) + + connection, err := grpc.NewClient(peerEndpoint, grpc.WithTransportCredentials(transportCredentials)) + if err != nil { + panic(fmt.Errorf("failed to create gRPC connection: %w", err)) + } + + return connection +} + +func newConnectOptions(clientConnection *grpc.ClientConn) (identity.Identity, []client.ConnectOption) { + return newIdentity(), []client.ConnectOption{ + client.WithSign(newSign()), + client.WithHash(hash.SHA256), + client.WithClientConnection(clientConnection), + client.WithEvaluateTimeout(5 * time.Second), + client.WithEndorseTimeout(15 * time.Second), + client.WithSubmitTimeout(5 * time.Second), + client.WithCommitStatusTimeout(1 * time.Minute), + } +} + +func newIdentity() *identity.X509Identity { + certificatePEM, err := os.ReadFile(certPath) + if err != nil { + panic(fmt.Errorf("failed to read certificate file: %w", err)) + } + + certificate, err := identity.CertificateFromPEM(certificatePEM) + if err != nil { + panic(err) + } + + id, err := identity.NewX509Identity(mspID, certificate) + if err != nil { + panic(err) + } + + return id +} + +func newSign() identity.Sign { + privateKeyPEM, err := readFirstFile(keyDirectoryPath) + if err != nil { + panic(fmt.Errorf("failed to read private key file: %w", err)) + } + + privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM) + if err != nil { + panic(err) + } + + sign, err := identity.NewPrivateKeySign(privateKey) + if err != nil { + panic(err) + } + + return sign +} + +func readFirstFile(dirPath string) ([]byte, error) { + dir, err := os.Open(dirPath) + if err != nil { + return nil, err + } + + fileNames, err := dir.Readdirnames(1) + if err != nil { + return nil, err + } + + return os.ReadFile(path.Join(dirPath, fileNames[0])) +} diff --git a/off_chain_data/application-go/contract.go b/off_chain_data/application-go/contract.go new file mode 100644 index 000000000..bc1f8d03f --- /dev/null +++ b/off_chain_data/application-go/contract.go @@ -0,0 +1,77 @@ +/* + * Copyright 2024 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "strconv" + + "github.com/hyperledger/fabric-gateway/pkg/client" +) + +type asset struct { + ID string + Color string + Size uint64 + Owner string + AppraisedValue uint64 +} + +type assetTransferBasic struct { + contract *client.Contract +} + +func newAssetTransferBasic(contract *client.Contract) *assetTransferBasic { + return &assetTransferBasic{contract} +} + +func (atb *assetTransferBasic) createAsset(anAsset asset) { + if _, err := atb.contract.Submit( + "CreateAsset", + client.WithArguments( + anAsset.ID, + anAsset.Color, + strconv.FormatUint(anAsset.Size, 10), + anAsset.Owner, + strconv.FormatUint(anAsset.AppraisedValue, 10), + )); err != nil { + panic(err) + } +} + +func (atb *assetTransferBasic) transferAsset(id, newOwner string) string { + result, err := atb.contract.Submit( + "TransferAsset", + client.WithArguments( + id, + newOwner, + ), + ) + if err != nil { + panic(err) + } + + return string(result) +} + +func (atb *assetTransferBasic) deleteAsset(id string) { + if _, err := atb.contract.Submit( + "DeleteAsset", + client.WithArguments( + id, + ), + ); err != nil { + panic(err) + } +} + +func (atb *assetTransferBasic) getAllAssets() []byte { + result, err := atb.contract.Evaluate("GetAllAssets") + if err != nil { + panic(err) + } + return result +} diff --git a/off_chain_data/application-go/getAllAssets.go b/off_chain_data/application-go/getAllAssets.go new file mode 100644 index 000000000..9c6643dae --- /dev/null +++ b/off_chain_data/application-go/getAllAssets.go @@ -0,0 +1,39 @@ +/* + * Copyright 2024 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/hyperledger/fabric-gateway/pkg/client" + "google.golang.org/grpc" +) + +func getAllAssets(clientConnection *grpc.ClientConn) { + id, options := newConnectOptions(clientConnection) + gateway, err := client.Connect(id, options...) + if err != nil { + panic((err)) + } + defer gateway.Close() + + contract := gateway.GetNetwork(channelName).GetContract(chaincodeName) + smartContract := newAssetTransferBasic(contract) + assets := smartContract.getAllAssets() + + fmt.Printf("%s\n", formatJSON(assets)) +} + +func formatJSON(data []byte) string { + var result bytes.Buffer + if err := json.Indent(&result, data, "", " "); err != nil { + panic(fmt.Errorf("failed to parse JSON: %w", err)) + } + return result.String() +} diff --git a/off_chain_data/application-go/go.mod b/off_chain_data/application-go/go.mod new file mode 100644 index 000000000..36ac5fff4 --- /dev/null +++ b/off_chain_data/application-go/go.mod @@ -0,0 +1,20 @@ +module offChainData + +go 1.22.0 + +require ( + github.com/hyperledger/fabric-gateway v1.7.0 + google.golang.org/grpc v1.67.1 +) + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/protobuf v1.35.1 // indirect +) diff --git a/off_chain_data/application-go/go.sum b/off_chain_data/application-go/go.sum new file mode 100644 index 000000000..c876dc53c --- /dev/null +++ b/off_chain_data/application-go/go.sum @@ -0,0 +1,34 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hyperledger/fabric-gateway v1.7.0 h1:bd1quU8qYPYqYO69m1tPIDSjB+D+u/rBJfE1eWFcpjY= +github.com/hyperledger/fabric-gateway v1.7.0/go.mod h1:TItDGnq71eJcgz5TW+m5Sq3kWGp0AEI1HPCNxj0Eu7k= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 h1:YJrd+gMaeY0/vsN0aS0QkEKTivGoUnSRIXxGJ7KI+Pc= +github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4/go.mod h1:bau/6AJhvEcu9GKKYHlDXAxXKzYNfhP6xu2GXuxEcFk= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/off_chain_data/application-go/listen.go b/off_chain_data/application-go/listen.go new file mode 100644 index 000000000..34f6f0efa --- /dev/null +++ b/off_chain_data/application-go/listen.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "fmt" + "math" + "strconv" + + "github.com/hyperledger/fabric-gateway/pkg/client" + "google.golang.org/grpc" +) + +var checkpointFile = envOrDefault("CHECKPOINT_FILE", "checkpoint.json") +var simulatedFailureCount = getSimulatedFailureCount() + +func listen(clientConnection *grpc.ClientConn) { + id, options := newConnectOptions(clientConnection) + gateway, err := client.Connect(id, options...) + if err != nil { + panic(err) + } + defer gateway.Close() + + network := gateway.GetNetwork(channelName) + + checkpointer, err := client.NewFileCheckpointer(checkpointFile) + if err != nil { + panic(err) + } + defer checkpointer.Close() + + fmt.Printf("Start event listening from block %d\n", checkpointer.BlockNumber()) + fmt.Printf("Last processed transaction ID within block: %s\n", checkpointer.TransactionID()) + if simulatedFailureCount > 0 { + fmt.Printf("Simulating a write failure every %d transactions", simulatedFailureCount) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + blocks, err := network.BlockEvents( + ctx, + client.WithStartBlock(0), + client.WithCheckpoint(checkpointer), + ) + if err != nil { + panic(err) + } + + for blockProto := range blocks { + checkpointer.CheckpointBlock(blockProto.GetHeader().GetNumber()) + } +} + +func getSimulatedFailureCount() uint { + valueAsString := envOrDefault("SIMULATED_FAILURE_COUNT", "0") + valueAsFloat, err := strconv.ParseFloat(valueAsString, 64) + if err != nil { + panic(err) + } + + result := math.Floor(valueAsFloat) + if valueAsFloat < 0 { + panic(fmt.Errorf("invalid SIMULATED_FAILURE_COUNT value: %s", valueAsString)) + } + + return uint(result) +} diff --git a/off_chain_data/application-go/transact.go b/off_chain_data/application-go/transact.go new file mode 100644 index 000000000..66b6e2fca --- /dev/null +++ b/off_chain_data/application-go/transact.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/hyperledger/fabric-gateway/pkg/client" + "google.golang.org/grpc" +) + +func transact(clientConnection *grpc.ClientConn) { + id, options := newConnectOptions(clientConnection) + gateway, err := client.Connect(id, options...) + if err != nil { + panic((err)) + } + defer gateway.Close() + + contract := gateway.GetNetwork(channelName).GetContract(chaincodeName) + + smartContract := newAssetTransferBasic(contract) + app := newTransactApp(smartContract) + app.run() +} + +type transactApp struct { + smartContract *assetTransferBasic + batchSize uint +} + +func newTransactApp(smartContract *assetTransferBasic) *transactApp { + return &transactApp{smartContract, 10} +} + +var ( + colors = []string{"red", "green", "blue"} + owners = []string{"alice", "bob", "charlie"} +) + +const ( + maxInitialValue = 1000 + maxInitialSize = 10 +) + +func (t *transactApp) run() { + for i := 0; i < int(t.batchSize); i++ { + go t.transact() + } +} + +func (t *transactApp) transact() { + anAsset := t.newAsset() + + t.smartContract.createAsset(anAsset) + fmt.Printf("\nCreated asset %s\n", anAsset.ID) + + // Transfer randomly 1 in 2 assets to a new owner. + if randomInt(2) == 0 { + newOwner := differentElement(owners, anAsset.Owner) + oldOwner := t.smartContract.transferAsset(anAsset.ID, newOwner) + fmt.Printf("Transferred asset %s from %s to %s\n", anAsset.ID, oldOwner, newOwner) + } + + // Delete randomly 1 in 4 created assets. + if randomInt(4) == 0 { + t.smartContract.deleteAsset(anAsset.ID) + fmt.Printf("Deleted asset %s\n", anAsset.ID) + } +} + +func (t *transactApp) newAsset() asset { + id, err := uuid.NewRandom() + if err != nil { + panic(err) + } + + return asset{ + ID: id.String(), + Color: randomElement(colors), + Size: uint64(randomInt(maxInitialSize) + 1), + Owner: randomElement(owners), + AppraisedValue: uint64(randomInt(maxInitialValue) + 1), + } +} diff --git a/off_chain_data/application-go/utils.go b/off_chain_data/application-go/utils.go new file mode 100644 index 000000000..c0c77af23 --- /dev/null +++ b/off_chain_data/application-go/utils.go @@ -0,0 +1,39 @@ +package main + +import ( + "crypto/rand" + "errors" + "math/big" +) + +func randomElement(values []string) string { + result := values[randomInt(len(values))] + return result +} + +func randomInt(max int) int { + result, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + if err != nil { + panic(err) + } + + return int(result.Int64()) +} + +func differentElement(values []string, currentValue string) string { + candidateValues := []string{} + for _, v := range values { + if v != currentValue { + candidateValues = append(candidateValues, v) + } + } + return randomElement(candidateValues) +} + +func assertDefined[T any](value T, message string) T { + if any(value) == any(nil) { + panic(errors.New(message)) + } + + return value +}