github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/docs/source/chaincode4ade.rst (about) 1 Writing Your First Chaincode 2 ============================ 3 4 What is Chaincode? 5 ------------------ 6 7 Chaincode is a program, written in `Go <https://golang.org>`_, `Node.js <https://nodejs.org>`_, 8 or `Java <https://java.com/en/>`_ that implements a prescribed interface. 9 Chaincode runs in a separate process from the peer and initializes and manages 10 the ledger state through transactions submitted by applications. 11 12 A chaincode typically handles business logic agreed to by members of the 13 network, so it similar to a "smart contract". A chaincode can be invoked to update or query 14 the ledger in a proposal transaction. Given the appropriate permission, a chaincode 15 may invoke another chaincode, either in the same channel or in different channels, to access its state. 16 Note that, if the called chaincode is on a different channel from the calling chaincode, 17 only read query is allowed. That is, the called chaincode on a different channel is only a ``Query``, 18 which does not participate in state validation checks in subsequent commit phase. 19 20 In the following sections, we will explore chaincode through the eyes of an 21 application developer. We'll present a asset-transfer chaincode sample walkthrough, 22 and the purpose of each method in the Fabric Contract API. If you 23 are a network operator who is deploying a chaincode to running network, 24 visit the :doc:`deploy_chaincode` tutorial and the :doc:`chaincode_lifecycle` 25 concept topic. 26 27 This tutorial provides an overview of the high level APIs provided by the Fabric Contract API. 28 To learn more about developing smart contracts using the Fabric contract API, visit the :doc:`developapps/smartcontract` topic. 29 30 Fabric Contract API 31 ------------------- 32 33 The ``fabric-contract-api`` provides the contract interface, a high level API for application developers to implement Smart Contracts. 34 Within Hechain, Smart Contracts are also known as Chaincode. Working with this API provides a high level entry point to writing business logic. 35 Documentation of the Fabric Contract API for different languages can be found at the links below: 36 37 - `Go <https://godoc.org/github.com/hyperledger/fabric-contract-api-go/contractapi>`__ 38 - `Node.js <https://hyperledger.github.io/fabric-chaincode-node/{BRANCH}/api/>`__ 39 - `Java <https://hyperledger.github.io/fabric-chaincode-java/{BRANCH}/api/org/hyperledger/fabric/contract/package-summary.html>`__ 40 41 42 Note that when using the contract api, each chaincode function that is called is passed a transaction context "ctx", from which 43 you can get the chaincode stub (GetStub() ), which has functions to access the ledger (e.g. GetState() ) and make requests 44 to update the ledger (e.g. PutState() ). You can learn more at the language-specific links below. 45 46 - `Go <https://godoc.org/github.com/hyperledger/fabric-chaincode-go/shim#Chaincode>`__ 47 - `Node.js <https://hyperledger.github.io/fabric-chaincode-node/{BRANCH}/api/fabric-shim.ChaincodeInterface.html>`__ 48 - `Java <https://hyperledger.github.io/fabric-chaincode-java/{BRANCH}/api/org/hyperledger/fabric/shim/Chaincode.html>`__ 49 50 51 In this tutorial using Go chaincode, we will demonstrate the use of these APIs 52 by implementing a asset-transfer chaincode application that manages simple "assets". 53 54 .. _Asset Transfer Chaincode: 55 56 Asset Transfer Chaincode 57 ------------------------ 58 Our application is a basic sample chaincode to initialize a ledger with assets, create, read, update, and delete assets, check to see 59 if an asset exists, and transfer assets from one owner to another. 60 61 Choosing a Location for the Code 62 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 63 64 If you haven't been doing programming in Go, you may want to make sure that 65 you have `Go <https://golang.org>`_ installed and your system properly configured. We assume 66 you are using a version that supports modules. 67 68 Now, you will want to create a directory for your chaincode application. 69 70 To keep things simple, let's use the following command: 71 72 .. code:: bash 73 74 // atcc is shorthand for asset transfer chaincode 75 mkdir atcc && cd atcc 76 77 Now, let's create the module and the source file that we'll fill in with code: 78 79 .. code:: bash 80 81 go mod init atcc 82 touch atcc.go 83 84 Housekeeping 85 ^^^^^^^^^^^^ 86 87 First, let's start with some housekeeping. As with every chaincode, it implements the 88 `fabric-contract-api interface <https://godoc.org/github.com/hyperledger/fabric-contract-api-go/contractapi>`_, 89 so let's add the Go import statements for the necessary dependencies for our chaincode. We'll import the 90 fabric contract api package and define our SmartContract. 91 92 .. code:: go 93 94 package main 95 96 import ( 97 "fmt" 98 "encoding/json" 99 "log" 100 "github.com/hyperledger/fabric-contract-api-go/contractapi" 101 ) 102 103 // SmartContract provides functions for managing an Asset 104 type SmartContract struct { 105 contractapi.Contract 106 } 107 108 Next, let's add a struct ``Asset`` to represent simple assets on the ledger. 109 Note the JSON annotations, which will be used to marshal the asset to JSON which is stored on the ledger. 110 JSON though is not a deterministic data format - the order of elements can change, whilst still representing the same data semantically. 111 The challenge, therefore, is to be able to generate a consistent set of JSON. 112 Below is also shown a good approach to achieve consistency which consists of creating an asset object struct following alphabetic order. 113 114 .. code:: go 115 116 // Asset describes basic details of what makes up a simple asset 117 // Insert struct field in alphabetic order => to achieve determinism accross languages 118 // golang keeps the order when marshal to json but doesn't order automatically 119 120 type Asset struct { 121 AppraisedValue int `json:"AppraisedValue"` 122 Color string `json:"Color"` 123 ID string `json:"ID"` 124 Owner string `json:"Owner"` 125 Size int `json:"Size"` 126 } 127 128 Initializing the Chaincode 129 ^^^^^^^^^^^^^^^^^^^^^^^^^^ 130 131 Next, we'll implement the ``InitLedger`` function to populate the ledger with some initial data. 132 133 .. code:: go 134 135 // InitLedger adds a base set of assets to the ledger 136 func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error { 137 assets := []Asset{ 138 {ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300}, 139 {ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400}, 140 {ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500}, 141 {ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600}, 142 {ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700}, 143 {ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800}, 144 } 145 146 for _, asset := range assets { 147 assetJSON, err := json.Marshal(asset) 148 if err != nil { 149 return err 150 } 151 152 err = ctx.GetStub().PutState(asset.ID, assetJSON) 153 if err != nil { 154 return fmt.Errorf("failed to put to world state. %v", err) 155 } 156 } 157 158 return nil 159 } 160 161 Next, we write a function to create an asset on the ledger that does not yet exist. When writing chaincode, it 162 is a good idea to check for the existence of something on the ledger prior to taking an action on it, as is demonstrated 163 in the ``CreateAsset`` function below. 164 165 166 .. code:: go 167 168 // CreateAsset issues a new asset to the world state with given details. 169 func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error { 170 exists, err := s.AssetExists(ctx, id) 171 if err != nil { 172 return err 173 } 174 if exists { 175 return fmt.Errorf("the asset %s already exists", id) 176 } 177 178 asset := Asset{ 179 ID: id, 180 Color: color, 181 Size: size, 182 Owner: owner, 183 AppraisedValue: appraisedValue, 184 } 185 assetJSON, err := json.Marshal(asset) 186 if err != nil { 187 return err 188 } 189 190 return ctx.GetStub().PutState(id, assetJSON) 191 } 192 193 Now that we have populated the ledger with some initial assets and created an asset, 194 let's write a function ``ReadAsset`` that allows us to read an asset from the ledger. 195 196 .. code:: go 197 198 // ReadAsset returns the asset stored in the world state with given id. 199 func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) { 200 assetJSON, err := ctx.GetStub().GetState(id) 201 if err != nil { 202 return nil, fmt.Errorf("failed to read from world state: %v", err) 203 } 204 if assetJSON == nil { 205 return nil, fmt.Errorf("the asset %s does not exist", id) 206 } 207 208 var asset Asset 209 err = json.Unmarshal(assetJSON, &asset) 210 if err != nil { 211 return nil, err 212 } 213 214 return &asset, nil 215 } 216 217 Now that we have assets on our ledger we can interact with, let's write a chaincode function 218 ``UpdateAsset`` that allows us to update attributes of the asset that we are allowed to change. 219 220 .. code:: go 221 222 // UpdateAsset updates an existing asset in the world state with provided parameters. 223 func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error { 224 exists, err := s.AssetExists(ctx, id) 225 if err != nil { 226 return err 227 } 228 if !exists { 229 return fmt.Errorf("the asset %s does not exist", id) 230 } 231 232 // overwriting original asset with new asset 233 asset := Asset{ 234 ID: id, 235 Color: color, 236 Size: size, 237 Owner: owner, 238 AppraisedValue: appraisedValue, 239 } 240 assetJSON, err := json.Marshal(asset) 241 if err != nil { 242 return err 243 } 244 245 return ctx.GetStub().PutState(id, assetJSON) 246 } 247 248 There may be cases where we need the ability to delete an asset from the ledger, 249 so let's write a ``DeleteAsset`` function to handle that requirement. 250 251 .. code:: go 252 253 // DeleteAsset deletes an given asset from the world state. 254 func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error { 255 exists, err := s.AssetExists(ctx, id) 256 if err != nil { 257 return err 258 } 259 if !exists { 260 return fmt.Errorf("the asset %s does not exist", id) 261 } 262 263 return ctx.GetStub().DelState(id) 264 } 265 266 267 We said earlier that is was a good idea to check to see if an asset exists before 268 taking an action on it, so let's write a function called ``AssetExists`` to implement that requirement. 269 270 .. code:: go 271 272 // AssetExists returns true when asset with given ID exists in world state 273 func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) { 274 assetJSON, err := ctx.GetStub().GetState(id) 275 if err != nil { 276 return false, fmt.Errorf("failed to read from world state: %v", err) 277 } 278 279 return assetJSON != nil, nil 280 } 281 282 Next, we'll write a function we'll call ``TransferAsset`` that enables the transfer of an asset from one owner to another. 283 284 .. code:: go 285 286 // TransferAsset updates the owner field of asset with given id in world state. 287 func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error { 288 asset, err := s.ReadAsset(ctx, id) 289 if err != nil { 290 return err 291 } 292 293 asset.Owner = newOwner 294 assetJSON, err := json.Marshal(asset) 295 if err != nil { 296 return err 297 } 298 299 return ctx.GetStub().PutState(id, assetJSON) 300 } 301 302 Let's write a function we'll call ``GetAllAssets`` that enables the querying of the ledger to 303 return all of the assets on the ledger. 304 305 .. code:: go 306 307 // GetAllAssets returns all assets found in world state 308 func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) { 309 // range query with empty string for startKey and endKey does an 310 // open-ended query of all assets in the chaincode namespace. 311 resultsIterator, err := ctx.GetStub().GetStateByRange("", "") 312 if err != nil { 313 return nil, err 314 } 315 defer resultsIterator.Close() 316 317 var assets []*Asset 318 for resultsIterator.HasNext() { 319 queryResponse, err := resultsIterator.Next() 320 if err != nil { 321 return nil, err 322 } 323 324 var asset Asset 325 err = json.Unmarshal(queryResponse.Value, &asset) 326 if err != nil { 327 return nil, err 328 } 329 assets = append(assets, &asset) 330 } 331 332 return assets, nil 333 } 334 335 .. _Chaincode Sample: 336 337 338 .. Note:: The full chaincode sample below is presented as a way to 339 to keep this tutorial as clear and straightforward as possible. In a 340 real-world implementation, it is likely that packages will be segmented 341 where a ``main`` package imports the chaincode package to allow for easy unit testing. 342 To see what this looks like, see the asset-transfer `Go chaincode <https://github.com/hyperledger/fabric-samples/tree/main/asset-transfer-basic/chaincode-go>`__ 343 in fabric-samples. If you look at ``assetTransfer.go``, you will see that 344 it contains ``package main`` and imports ``package chaincode`` defined in ``smartcontract.go`` and 345 located at ``fabric-samples/asset-transfer-basic/chaincode-go/chaincode/``. 346 347 348 349 Pulling it All Together 350 ^^^^^^^^^^^^^^^^^^^^^^^ 351 352 Finally, we need to add the ``main`` function, which will call the 353 `ContractChaincode.Start <https://godoc.org/github.com/hyperledger/fabric-contract-api-go/contractapi#ContractChaincode.Start>`_ 354 function. Here's the whole chaincode program source. 355 356 .. code:: go 357 358 package main 359 360 import ( 361 "encoding/json" 362 "fmt" 363 "log" 364 365 "github.com/hyperledger/fabric-contract-api-go/contractapi" 366 ) 367 368 // SmartContract provides functions for managing an Asset 369 type SmartContract struct { 370 contractapi.Contract 371 } 372 373 // Asset describes basic details of what makes up a simple asset 374 type Asset struct { 375 ID string `json:"ID"` 376 Color string `json:"color"` 377 Size int `json:"size"` 378 Owner string `json:"owner"` 379 AppraisedValue int `json:"appraisedValue"` 380 } 381 382 // InitLedger adds a base set of assets to the ledger 383 func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error { 384 assets := []Asset{ 385 {ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300}, 386 {ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400}, 387 {ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500}, 388 {ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600}, 389 {ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700}, 390 {ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800}, 391 } 392 393 for _, asset := range assets { 394 assetJSON, err := json.Marshal(asset) 395 if err != nil { 396 return err 397 } 398 399 err = ctx.GetStub().PutState(asset.ID, assetJSON) 400 if err != nil { 401 return fmt.Errorf("failed to put to world state. %v", err) 402 } 403 } 404 405 return nil 406 } 407 408 // CreateAsset issues a new asset to the world state with given details. 409 func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error { 410 exists, err := s.AssetExists(ctx, id) 411 if err != nil { 412 return err 413 } 414 if exists { 415 return fmt.Errorf("the asset %s already exists", id) 416 } 417 418 asset := Asset{ 419 ID: id, 420 Color: color, 421 Size: size, 422 Owner: owner, 423 AppraisedValue: appraisedValue, 424 } 425 assetJSON, err := json.Marshal(asset) 426 if err != nil { 427 return err 428 } 429 430 return ctx.GetStub().PutState(id, assetJSON) 431 } 432 433 // ReadAsset returns the asset stored in the world state with given id. 434 func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) { 435 assetJSON, err := ctx.GetStub().GetState(id) 436 if err != nil { 437 return nil, fmt.Errorf("failed to read from world state: %v", err) 438 } 439 if assetJSON == nil { 440 return nil, fmt.Errorf("the asset %s does not exist", id) 441 } 442 443 var asset Asset 444 err = json.Unmarshal(assetJSON, &asset) 445 if err != nil { 446 return nil, err 447 } 448 449 return &asset, nil 450 } 451 452 // UpdateAsset updates an existing asset in the world state with provided parameters. 453 func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error { 454 exists, err := s.AssetExists(ctx, id) 455 if err != nil { 456 return err 457 } 458 if !exists { 459 return fmt.Errorf("the asset %s does not exist", id) 460 } 461 462 // overwriting original asset with new asset 463 asset := Asset{ 464 ID: id, 465 Color: color, 466 Size: size, 467 Owner: owner, 468 AppraisedValue: appraisedValue, 469 } 470 assetJSON, err := json.Marshal(asset) 471 if err != nil { 472 return err 473 } 474 475 return ctx.GetStub().PutState(id, assetJSON) 476 } 477 478 // DeleteAsset deletes an given asset from the world state. 479 func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error { 480 exists, err := s.AssetExists(ctx, id) 481 if err != nil { 482 return err 483 } 484 if !exists { 485 return fmt.Errorf("the asset %s does not exist", id) 486 } 487 488 return ctx.GetStub().DelState(id) 489 } 490 491 // AssetExists returns true when asset with given ID exists in world state 492 func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) { 493 assetJSON, err := ctx.GetStub().GetState(id) 494 if err != nil { 495 return false, fmt.Errorf("failed to read from world state: %v", err) 496 } 497 498 return assetJSON != nil, nil 499 } 500 501 // TransferAsset updates the owner field of asset with given id in world state. 502 func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error { 503 asset, err := s.ReadAsset(ctx, id) 504 if err != nil { 505 return err 506 } 507 508 asset.Owner = newOwner 509 assetJSON, err := json.Marshal(asset) 510 if err != nil { 511 return err 512 } 513 514 return ctx.GetStub().PutState(id, assetJSON) 515 } 516 517 // GetAllAssets returns all assets found in world state 518 func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) { 519 // range query with empty string for startKey and endKey does an 520 // open-ended query of all assets in the chaincode namespace. 521 resultsIterator, err := ctx.GetStub().GetStateByRange("", "") 522 if err != nil { 523 return nil, err 524 } 525 defer resultsIterator.Close() 526 527 var assets []*Asset 528 for resultsIterator.HasNext() { 529 queryResponse, err := resultsIterator.Next() 530 if err != nil { 531 return nil, err 532 } 533 534 var asset Asset 535 err = json.Unmarshal(queryResponse.Value, &asset) 536 if err != nil { 537 return nil, err 538 } 539 assets = append(assets, &asset) 540 } 541 542 return assets, nil 543 } 544 545 func main() { 546 assetChaincode, err := contractapi.NewChaincode(&SmartContract{}) 547 if err != nil { 548 log.Panicf("Error creating asset-transfer-basic chaincode: %v", err) 549 } 550 551 if err := assetChaincode.Start(); err != nil { 552 log.Panicf("Error starting asset-transfer-basic chaincode: %v", err) 553 } 554 } 555 556 Chaincode access control 557 ------------------------ 558 559 Chaincode can utilize the client (submitter) certificate for access 560 control decisions with ``ctx.GetStub().GetCreator()``. Additionally 561 the Fabric Contract API provides extension APIs that extract client identity 562 from the submitter's certificate that can be used for access control decisions, 563 whether that is based on client identity itself, or the org identity, 564 or on a client identity attribute. 565 566 For example an asset that is represented as a key/value may include the 567 client's identity as part of the value (for example as a JSON attribute 568 indicating that asset owner), and only this client may be authorized 569 to make updates to the key/value in the future. The client identity 570 library extension APIs can be used within chaincode to retrieve this 571 submitter information to make such access control decisions. 572 573 574 .. _vendoring: 575 576 Managing external dependencies for chaincode written in Go 577 ---------------------------------------------------------- 578 Your Go chaincode depends on Go packages (like the chaincode shim) that are not 579 part of the standard library. The source to these packages must be included in 580 your chaincode package when it is installed to a peer. If you have structured 581 your chaincode as a module, the easiest way to do this is to "vendor" the 582 dependencies with ``go mod vendor`` before packaging your chaincode. 583 584 .. code:: bash 585 586 go mod tidy 587 go mod vendor 588 589 This places the external dependencies for your chaincode into a local ``vendor`` 590 directory. 591 592 Once dependencies are vendored in your chaincode directory, ``peer chaincode package`` 593 and ``peer chaincode install`` operations will then include code associated with the 594 dependencies into the chaincode package. 595 596 JSON determinism 597 ---------------- 598 Being able to predictably handle data formats is critical, and also the ability to search the data held within the blockchain. 599 600 Technical Problem 601 ^^^^^^^^^^^^^^^^^ 602 The format of the data that is stored in Fabric is at the discretion of the user. 603 The lowest level API accepts a byte array and stores that - what this represents is not a concern to Fabric. 604 The important thing is when simulating transactions, given the same inputs chaincode gives the same byte array. 605 Otherwise, the endorsements may not all match and the transaction will either not be submitted or will be invalidated. 606 607 JSON is often used as the data format to store data on the ledger, and is required if using CouchDB queries. 608 609 JSON though is not a deterministic data format - the order of elements can change, 610 whilst still representing the same data semantically. The challenge, therefore, is to be able to generate a consistent set of JSON. 611 612 A solution 613 ^^^^^^^^^^ 614 Generate a consistent set of ``JSON`` across multiple languages. 615 Each language have different features and libraries that you can use to convert an object to JSON. 616 The best approach to achieve determinism across different languages is to choose a canonical way as a common guideline to format JSON. 617 In order to get a consistent hash across languages you can format JSON in alphabetic order. 618 619 Golang 620 ^^^^^^ 621 In Golang the ``encoding/json`` package is utilized to serialise a Struct Object into JSON. 622 More specifically the ``Marshal`` function is used, the latter marshals maps in sorted key order and keeps structs in the order that the fields are declared. 623 Since structs are marshaled in field declaration order, follow alphabetic order when defining a new structure. 624 625 Node.js 626 ^^^^^^^ 627 In Javascript, when serialising object into JSON, the function ``JSON.stringify()`` is commonly used. 628 However, to achieve consistent results, a deterministic version of JSON.stringify() is needed; in this way it is possible to get a consistent hash from stringified results. 629 ``json-stringify-deterministic`` is a good library to do so and can be used combined with ``sort-keys-recursive`` to attain alphabetic order too. 630 `Here <https://www.npmjs.com/package/json-stringify-deterministic>`_ for a more in-depth tutorial. 631 632 Java 633 ^^^^ 634 Java provides several libraries to serialize an object into a JSON string. However not all of them provide consistency and ordering. 635 The ``Gson`` library, for example, does not provide any consistency and should therefore be avoided for this application. On the other hand, 636 the ``Genson`` library is a good fit for our purpose as it produces consistent JSON in alphabetic oreder. 637 638 You can find a good exemplification of this practise on the `asset-transfer-basic <https://github.com/hyperledger/fabric-samples/tree/main/asset-transfer-basic>`_ chaincodes. 639 640 .. Note:: 641 This is only one of the many approaches which we think can be effective. 642 When serialising you can utilise various methods to achieve consistency; nevertheless, 643 considering the different characteristics of the programming languages used in Fabric, 644 the alphabetic approach represents an easy and efficient solution to the problem. 645 In conclusion, feel free to employ a different method if it best suites your needs. 646 P.S. Don’t forget to let us know in the comments if you used a different approach. 647 648 .. Licensed under Creative Commons Attribution 4.0 International License 649 https://creativecommons.org/licenses/by/4.0/