├── .gitignore ├── settings.gradle ├── .dockerignore ├── docs └── images │ └── architecture.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitattributes ├── src └── main │ ├── resources │ └── db │ │ └── migration │ │ ├── V2__client_id_and_transaction_hash.sql │ │ └── V1__initial.sql │ └── java │ └── com │ └── hedera │ └── hashgraph │ └── poa │ ├── CreateActionResponse.java │ ├── SearchActionRequest.java │ ├── InstantConverter.java │ ├── CreateActionRequest.java │ ├── SearchActionResponse.java │ ├── SearchActionHandler.java │ ├── App.java │ └── CreateActionHandler.java ├── .env.sample ├── docker-compose.yaml ├── .github └── workflows │ └── docker.yml ├── Dockerfile ├── PostmanExamples.json ├── gradlew.bat ├── gradlew ├── README.md ├── CONTRIBUTING.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | .idea/ 4 | .env 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'hedera-proof-of-action' 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !src/ 3 | !gradle/ 4 | !build.gradle 5 | !gradlew 6 | !settings.gradle 7 | 8 | -------------------------------------------------------------------------------- /docs/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashgraph/hedera-proof-of-action-microservice/HEAD/docs/images/architecture.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashgraph/hedera-proof-of-action-microservice/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V2__client_id_and_transaction_hash.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE actions ADD COLUMN client_id TEXT; 2 | 3 | CREATE INDEX ON actions (client_id); 4 | 5 | ALTER TABLE proofs ADD COLUMN transaction_hash BYTEA; 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 23 01:20:20 PDT 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /src/main/java/com/hedera/hashgraph/poa/CreateActionResponse.java: -------------------------------------------------------------------------------- 1 | package com.hedera.hashgraph.poa; 2 | 3 | import com.hedera.hashgraph.sdk.TransactionId; 4 | 5 | public class CreateActionResponse { 6 | public final String transactionId; 7 | 8 | CreateActionResponse(TransactionId transactionId) { 9 | this.transactionId = transactionId.toString(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/hedera/hashgraph/poa/SearchActionRequest.java: -------------------------------------------------------------------------------- 1 | package com.hedera.hashgraph.poa; 2 | 3 | public class SearchActionRequest { 4 | public String payload; 5 | public String transactionId; 6 | public String clientId; 7 | 8 | SearchActionRequest() { 9 | } 10 | 11 | SearchActionRequest(String payload, String transactionId, String clientId) { 12 | this.payload = payload; 13 | this.clientId = clientId; 14 | this.transactionId = transactionId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # Database 2 | DATABASE_URL=postgresql://db:5432/hedera_proof_of_action__dev 3 | DATABASE_USER=postgres 4 | DATABASE_PASSWORD=password 5 | 6 | # Hedera Operator to use to submit transactions to the network 7 | HEDERA_OPERATOR_ID=0.0.xxx 8 | HEDERA_OPERATOR_KEY=302... 9 | 10 | # Network to use, testnet, mainnet or previewnet 11 | HEDERA_NETWORK=testnet 12 | 13 | # Topic ID to use to submit messages to, if unspecified a new one will be 14 | # created for you and will be logged to the console 15 | HEDERA_TOPIC_ID=0.0.yyy 16 | 17 | # Secret key to use for AES encryption 18 | SECRET_KEY=... 19 | -------------------------------------------------------------------------------- /src/main/java/com/hedera/hashgraph/poa/InstantConverter.java: -------------------------------------------------------------------------------- 1 | package com.hedera.hashgraph.poa; 2 | 3 | import java.time.Instant; 4 | 5 | public class InstantConverter { 6 | private InstantConverter() {} 7 | 8 | // converts a Java Instant to nanoseconds for storage 9 | public static long toNanos(Instant instant) { 10 | return (instant.getEpochSecond() * 1000000000) + instant.getNano(); 11 | } 12 | 13 | public static Instant fromNanos(long nanos) { 14 | var seconds = nanos / 1000000000; 15 | var fracNanos = nanos % 1000000000; 16 | 17 | return Instant.ofEpochSecond(seconds, fracNanos); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | db: 4 | image: postgres:12 5 | restart: on-failure 6 | environment: 7 | POSTGRES_PASSWORD: password 8 | POSTGRES_DB: hedera_proof_of_action__dev 9 | healthcheck: 10 | test: ["CMD-SHELL", "pg_isready -U postgres"] 11 | interval: 10s 12 | timeout: 5s 13 | retries: 5 14 | 15 | poa: 16 | build: 17 | context: . 18 | restart: on-failure 19 | ports: 20 | - "8080:8080" 21 | depends_on: 22 | - db 23 | env_file: 24 | - .env 25 | environment: 26 | - DATABASE_URL=postgresql://db:5432/hedera_proof_of_action__dev 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/com/hedera/hashgraph/poa/CreateActionRequest.java: -------------------------------------------------------------------------------- 1 | package com.hedera.hashgraph.poa; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class CreateActionRequest { 6 | public String payload; 7 | 8 | // optional, non-unique, client-generated ID 9 | // this can be used to filter the actions by at a later time 10 | public String clientId; 11 | 12 | public CreateActionRequestSubmit submit = CreateActionRequestSubmit.DIRECT; 13 | 14 | public enum CreateActionRequestSubmit { 15 | @JsonProperty("direct") DIRECT, 16 | @JsonProperty("hash") HASH, 17 | @JsonProperty("encrypted") ENCRYPTED 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Harden Runner 13 | uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 14 | with: 15 | egress-policy: audit 16 | 17 | - name: Checkout code 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | 20 | - name: Upload Docker image 21 | uses: mattdavis0351/actions/docker-gpr@7fc5cbf3cc2defc2361f624fc2c2f22a686f4222 # v1.0.0 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | image-name: hedera-proof-of-action 25 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__initial.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE actions 2 | ( 3 | id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, 4 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 5 | 6 | -- original payload submitted with the action 7 | payload TEXT NOT NULL, 8 | 9 | -- transaction ID of the message submit transaction 10 | transaction_id_num BIGINT NOT NULL, 11 | transaction_id_valid_start BIGINT NOT NULL, 12 | 13 | UNIQUE (transaction_id_num, transaction_id_valid_start) 14 | ); 15 | 16 | -- CREATE INDEX ON actions (payload); 17 | CREATE INDEX ON actions (transaction_id_valid_start, transaction_id_num); 18 | 19 | CREATE TABLE proofs 20 | ( 21 | action_id BIGINT PRIMARY KEY REFERENCES actions (id), 22 | created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 23 | consensus_timestamp BIGINT NOT NULL, 24 | sequence_number BIGINT NOT NULL, 25 | running_hash BYTEA NOT NULL 26 | ); 27 | 28 | CREATE INDEX ON proofs (action_id); 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # build 3 | # 4 | 5 | FROM adoptopenjdk:14-jdk-hotspot AS build 6 | 7 | # download and cache the version of gradle in use 8 | COPY ./gradle /opt/hedera-proof-of-action/gradle 9 | COPY ./gradlew /opt/hedera-proof-of-action/gradlew 10 | 11 | RUN cd /opt/hedera-proof-of-action && ./gradlew --no-daemon 12 | 13 | # copy in just enough to cache dependencies 14 | COPY ./build.gradle /opt/hedera-proof-of-action/build.gradle 15 | COPY ./settings.gradle /opt/hedera-proof-of-action/settings.gradle 16 | 17 | RUN cd /opt/hedera-proof-of-action && ./gradlew --no-daemon compileJava 18 | 19 | # now, finally copy in the source and build a JAR 20 | COPY ./src /opt/hedera-proof-of-action/src 21 | 22 | RUN cd /opt/hedera-proof-of-action && ./gradlew --no-daemon build 23 | 24 | # 25 | # run 26 | # 27 | 28 | FROM adoptopenjdk:14-jre-hotspot 29 | 30 | # make a place to put our built JAR and copy it to there 31 | WORKDIR /srv 32 | COPY --from=build /opt/hedera-proof-of-action/build/libs/hedera-proof-of-action.jar /srv/hedera-proof-of-action.jar 33 | 34 | # run the micro service 35 | CMD java "-jar" "hedera-proof-of-action.jar" 36 | EXPOSE 8080 37 | -------------------------------------------------------------------------------- /src/main/java/com/hedera/hashgraph/poa/SearchActionResponse.java: -------------------------------------------------------------------------------- 1 | package com.hedera.hashgraph.poa; 2 | 3 | import com.google.common.hash.HashCode; 4 | import com.hedera.hashgraph.sdk.AccountId; 5 | import com.hedera.hashgraph.sdk.TransactionId; 6 | import io.vertx.sqlclient.Row; 7 | 8 | import java.time.Instant; 9 | import java.time.format.DateTimeFormatter; 10 | 11 | public class SearchActionResponse { 12 | public final String transactionId; 13 | 14 | public final String consensusTimestamp; 15 | 16 | public final long sequenceNumber; 17 | 18 | public final String runningHash; 19 | 20 | public final String clientId; 21 | 22 | public final String transactionHash; 23 | 24 | SearchActionResponse(Row row) { 25 | var transactionAccountId = row.getLong("transaction_id_num"); 26 | var validStart = row.getLong("transaction_id_valid_start"); 27 | var seqNum = row.getLong("sequence_number"); 28 | var runningHash = row.getBuffer("running_hash"); 29 | var transactionHash = row.getBuffer("transaction_hash"); 30 | var consensusTimestamp = row.getLong("consensus_timestamp"); 31 | 32 | this.transactionId = new TransactionId( 33 | new AccountId(transactionAccountId), 34 | Instant.ofEpochSecond(0, validStart) 35 | ).toString(); 36 | 37 | this.sequenceNumber = seqNum; 38 | this.clientId = row.getString("client_id"); 39 | 40 | // noinspection UnstableApiUsage 41 | this.runningHash = HashCode.fromBytes(runningHash.getBytes()).toString(); 42 | 43 | if (transactionHash != null) { 44 | // noinspection UnstableApiUsage 45 | this.transactionHash = HashCode.fromBytes(transactionHash.getBytes()).toString(); 46 | } else { 47 | this.transactionHash = null; 48 | } 49 | 50 | this.consensusTimestamp = DateTimeFormatter.ISO_INSTANT.format(InstantConverter.fromNanos(consensusTimestamp)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /PostmanExamples.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "7b662259-e5b3-4243-95b6-9d0094425a5b", 4 | "name": "POA", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Submit", 10 | "event": [ 11 | { 12 | "listen": "test", 13 | "script": { 14 | "id": "5ba2db0d-c138-4f61-a726-3fbd77539c81", 15 | "exec": [ 16 | "const responseJson = pm.response.json();", 17 | "const txId = responseJson.transactionId", 18 | "", 19 | "pm.collectionVariables.set('transactionId', txId);", 20 | "" 21 | ], 22 | "type": "text/javascript" 23 | } 24 | } 25 | ], 26 | "request": { 27 | "method": "POST", 28 | "header": [], 29 | "body": { 30 | "mode": "raw", 31 | "raw": "{\n \"payload\": \"{{payload}}\",\n \"submit\": \"direct\"\n}", 32 | "options": { 33 | "raw": { 34 | "language": "json" 35 | } 36 | } 37 | }, 38 | "url": { 39 | "raw": "{{baseUrl}}/v1/action/", 40 | "host": [ 41 | "{{baseUrl}}" 42 | ], 43 | "path": [ 44 | "v1", 45 | "action", 46 | "" 47 | ] 48 | } 49 | }, 50 | "response": [] 51 | }, 52 | { 53 | "name": "GetByTXId", 54 | "request": { 55 | "method": "GET", 56 | "header": [], 57 | "url": { 58 | "raw": "" 59 | } 60 | }, 61 | "response": [] 62 | }, 63 | { 64 | "name": "GetByPayload", 65 | "request": { 66 | "method": "GET", 67 | "header": [], 68 | "url": { 69 | "raw": "" 70 | } 71 | }, 72 | "response": [] 73 | } 74 | ], 75 | "event": [ 76 | { 77 | "listen": "prerequest", 78 | "script": { 79 | "id": "b24883e3-0685-4708-9c33-58b2515c53ff", 80 | "type": "text/javascript", 81 | "exec": [ 82 | "" 83 | ] 84 | } 85 | }, 86 | { 87 | "listen": "test", 88 | "script": { 89 | "id": "c00dc31c-1b50-45bd-a6c1-fd77ae88f5f5", 90 | "type": "text/javascript", 91 | "exec": [ 92 | "" 93 | ] 94 | } 95 | } 96 | ], 97 | "variable": [ 98 | { 99 | "id": "ae274fad-399e-4775-a800-c942d9efd485", 100 | "key": "baseUrl", 101 | "value": "http://localhost:8080" 102 | }, 103 | { 104 | "id": "d1e2825e-ef6c-4e78-ad44-dff7f20f8625", 105 | "key": "transactionId", 106 | "value": "0.0.11093@1601996102.287911702" 107 | }, 108 | { 109 | "id": "5db76dc5-d758-42a9-b6ec-02f21f268985", 110 | "key": "payload", 111 | "value": "anything goes here" 112 | } 113 | ], 114 | "protocolProfileBehavior": {} 115 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /src/main/java/com/hedera/hashgraph/poa/SearchActionHandler.java: -------------------------------------------------------------------------------- 1 | package com.hedera.hashgraph.poa; 2 | 3 | import com.google.common.flogger.FluentLogger; 4 | import com.hedera.hashgraph.sdk.AccountId; 5 | import com.hedera.hashgraph.sdk.TransactionId; 6 | import io.vertx.core.Handler; 7 | import io.vertx.core.Promise; 8 | import io.vertx.core.http.HttpMethod; 9 | import io.vertx.core.json.Json; 10 | import io.vertx.ext.web.RoutingContext; 11 | import io.vertx.pgclient.PgPool; 12 | import io.vertx.sqlclient.RowSet; 13 | import io.vertx.sqlclient.impl.ArrayTuple; 14 | 15 | import java.time.Instant; 16 | import java.util.ArrayList; 17 | import java.util.stream.Collectors; 18 | import java.util.stream.StreamSupport; 19 | 20 | public class SearchActionHandler implements Handler { 21 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 22 | 23 | private final PgPool db; 24 | 25 | public SearchActionHandler(PgPool db) { 26 | this.db = db; 27 | } 28 | 29 | @Override 30 | public void handle(RoutingContext rx) { 31 | var res = rx.response(); 32 | 33 | SearchActionRequest req; 34 | 35 | if (rx.request().method() == HttpMethod.GET) { 36 | req = new SearchActionRequest( 37 | rx.request().getParam("payload"), 38 | rx.request().getParam("transactionId"), 39 | rx.request().getParam("clientId") 40 | ); 41 | } else { 42 | var reqBody = rx.getBody(); 43 | if (reqBody.length() == 0) { 44 | // no content, instant failure 45 | res.setStatusCode(400); 46 | res.end(); 47 | return; 48 | } 49 | 50 | req = Json.decodeValue(reqBody, SearchActionRequest.class); 51 | } 52 | 53 | try { 54 | var handler = Promise.>promise(); 55 | 56 | var clause = new ArrayList(); 57 | var args = new ArrayTuple(3); 58 | 59 | if (req.clientId != null) { 60 | clause.add(String.format("(a.client_id = $%d)", args.size() + 1)); 61 | args.addValue(req.clientId); 62 | } 63 | 64 | if (req.payload != null) { 65 | clause.add(String.format("(a.payload = $%d)", args.size() + 1)); 66 | args.addValue(req.payload); 67 | } 68 | 69 | if (req.transactionId != null) { 70 | var transactionId = transactionIdFromString(req.transactionId); 71 | clause.add(String.format("(a.transaction_id_num = $%d AND a.transaction_id_valid_start = $%d)", 72 | args.size() + 1, args.size() + 2)); 73 | 74 | args.addValue(transactionId.accountId.num); 75 | args.addValue(InstantConverter.toNanos(transactionId.validStart)); 76 | } 77 | 78 | if (args.size() == 0) { 79 | // need at least one query parameter 80 | res.setStatusCode(400).end(); 81 | return; 82 | } 83 | 84 | db.preparedQuery( 85 | "SELECT a.transaction_id_num, a.transaction_id_valid_start, p.sequence_number, p.running_hash, p.consensus_timestamp, p.transaction_hash, a.client_id " + 86 | "FROM proofs p " + 87 | "INNER JOIN actions a ON a.id = p.action_id " + 88 | "WHERE " + String.join(" AND ", clause) + " " + 89 | "ORDER BY p.consensus_timestamp DESC" 90 | ).mapping(SearchActionResponse::new).execute(args, handler); 91 | 92 | handler.future().onComplete(v -> { 93 | if (v.failed()) { 94 | rx.fail(v.cause()); 95 | return; 96 | } 97 | 98 | var rows = v.result(); 99 | var results = StreamSupport.stream(rows.spliterator(), false).collect(Collectors.toUnmodifiableList()); 100 | 101 | if (results.size() == 0) { 102 | // no dice 103 | res.setStatusCode(404).end(); 104 | return; 105 | } 106 | 107 | res.setStatusCode(200) 108 | .putHeader("content-type", "application/json") 109 | .end(Json.encode(results)); 110 | }); 111 | } catch (Exception e) { 112 | e.printStackTrace(); 113 | } 114 | } 115 | 116 | // convert a String to a Hedera Transaction ID 117 | // FIXME: put in the SDK 118 | private TransactionId transactionIdFromString(String s) { 119 | // FIXME: error handling here 120 | var parts = s.split("@"); 121 | var accountIdS = parts[0]; 122 | var timestampParts = parts[1].split("\\."); 123 | var secondsS = timestampParts[0]; 124 | var nanosS = timestampParts[1]; 125 | 126 | var timestamp = Instant.ofEpochSecond(Long.parseLong(secondsS), Long.parseLong(nanosS)); 127 | var accountId = AccountId.fromString(accountIdS); 128 | 129 | return new TransactionId(accountId, timestamp); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hedera™ Hashgraph Proof-of-Action Microservice 2 | 3 | ## What is the proof-of-action microservice? 4 | The Hedera Proof of Action (HPoA) microservice aims to make it easy for organizations to build a capability to record and subsequently prove the existence of business events using the Hedera Consensus Service. 5 | 6 | Examples of these types of recorded events are proof of deletion of personally sensitive data for regulatory compliance, proof of usage of data for a particular purpose for record keeping, or proof of redeeming a coupon for business process automation. 7 | 8 | ## How does the proof-of-action microservice work? 9 | First, an application logs important business events (serialized business objects) into the HPoA microservice at the time they occur. The business logic specifies whether the business event should be stored in open, encrypted, or in the hashed form. The HPoA microservice stores these events in a specific HCS topic, configured for this purpose. Creation of this topic is done as part of initialization. 10 | 11 | When the business or third party needs to prove the existence of an event – say for creating a report, for an audit, or in a court of law — it simply invokes another API with either the business object or an identifier to that object, and the HPoA microservice provides a mathematically provable record of that business object being stored on to the public ledger along with the timestamp. 12 | 13 | ## Microservice architecture diagram 14 | First, a business application creates a business object that needs to be recorded for the purposes of compliance, auditability, public verifiability, automation, or other regulatory reasons. The business logic simply calls a /record_action API with a serialized representation of that business object. The following options are available to the business logic: 15 | 16 | #### Storage in the public ledger: 17 | * Store the object in unencrypted form 18 | * Store the object in an encrypted form 19 | * Store just the hash of the object 20 | 21 | #### Storage in a database within the microservice: 22 | * Option to store the business object in a database within the microservice 23 | 24 | When the business application invokes this API, the appropriate data (original object, encrypted object, or a hash) is sent to Hedera Consensus Service using the SDK. If required, the SDK handles fragmentation of messages. The microservice returns the Transaction-ID that the business logic stores for future references. 25 | 26 | When the business application wants to prove that a particular event was memorialized on the public DLT, it invokes the prove_action API – either with the original business object, or with the Transction-ID. The HPoA microservice queries a mirror node (mirror nodes are run by Hedera as well as several third parties), and obtains the record of the transaction. It also creates an equivalent representation of the original object (encryption or hashed), and compares it with the representation obtained from the mirror node record. If the two match, it returns a successful response with the business object that was memorialized and appropriate details of the proof from the Hedera’s public ledger. In future, this proof will also contain the state proof obtained from Hedera. 27 | 28 | ![Image of HPoA microservice architecture diagram](docs/images/architecture.png) 29 | 30 | ## Requirements 31 | 32 | * Java 14 33 | * Docker 34 | * Docker-compose 35 | 36 | ## Install 37 | 38 | setup environment variables 39 | 40 | ```shell script 41 | $ cd hedera-proof-of-action-microservice 42 | $ cp .env.sample .env 43 | $ nano .env 44 | ``` 45 | 46 | Edit the following environment variables 47 | 48 | * `HEDERA_OPERATOR_ID` – The Account ID on Hedera™ that will pay for the transactions. 49 | 50 | * `HEDERA_OPERATOR_KEY` – The matching private key for the `HEDERA_OPERATOR_ID`. 51 | 52 | * `HEDERA_NETWORK` - The Hedera Network to use: `testnet`, `mainnet` or `previewnet` 53 | 54 | * `HEDERA_TOPIC_ID` – The topic ID to use for consensus. A new one will be created if not provided. 55 | 56 | * `SECRET_KEY` – The secret key to use to optionally encrypt messages to Hedera. A new one will be generated if not provided. 57 | 58 | * Note: You may leave secret key blank to have one generated by the system and output in the console * 59 | 60 | ```sh 61 | $ docker-compose up 62 | ``` 63 | 64 | ## Usage 65 | 66 | ### Submit an action 67 | 68 | An action can be submitted to HCS as a `hash`, `encrypted`, or `direct` ( 69 | `direct` is the default). 70 | 71 | An optional, client-generated, non-unique `clientId` can be submitted and later used to lookup actions. 72 | 73 | ```json 74 | POST /v1/action/ 75 | content-type: application/json 76 | 77 | { 78 | "payload": "anything goes here", 79 | "submit": "hash" 80 | } 81 | ``` 82 | 83 | ```json 84 | 202 Accepted 85 | content-length: 49 86 | content-type: application/json 87 | 88 | { 89 | "transactionId": "0.0.1035@1587742118.141000000" 90 | } 91 | ``` 92 | 93 | #### INVALID_TRANSACTION_START errors 94 | 95 | In some instances, the clock inside the docker proof of action container drifts, resulting in an error similar to `failed pre-check with the status INVALID_TRANSACTION_START` 96 | 97 | running the command below will re-synchronise the clock in the container (no need to restart it) 98 | 99 | ```shell script 100 | docker run --rm --privileged hedera-proof-of-action-microservice_poa hwclock -s 101 | ``` 102 | 103 | ### Prove an action 104 | 105 | An action can be proven by requesting by the original payload, the returned transaction ID, or a client-generated ID. 106 | 107 | ```json 108 | GET /v1/action/?payload=anything%20goes%20here 109 | ``` 110 | 111 | ```json 112 | GET /v1/action/?transactionId=0.0.1035@1587742118.141000000 113 | ``` 114 | 115 | ```json 116 | 200 OK 117 | content-length: 235 118 | content-type: application/json 119 | 120 | [ 121 | { 122 | "consensusTimestamp": "2020-04-24T15:28:48.595593Z", 123 | "runningHash": "2dc9abcfea672d0d6047504dada83e4540f3a28601af7b6e9eaaf071e570b3624d5f22d7d0caa7944e00ee6fb11f9392", 124 | "sequenceNumber": 19, 125 | "transactionId": "0.0.1035@1587742118.141000000" 126 | } 127 | ] 128 | ``` 129 | 130 | ## Contributing to this Project 131 | 132 | We welcome participation from all developers! 133 | For instructions on how to contribute to this repo, please 134 | review the [Contributing Guide](CONTRIBUTING.md). 135 | 136 | ## License Information 137 | 138 | Licensed under Apache License, 139 | Version 2.0 – see [LICENSE](LICENSE) in this repo 140 | or [apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). 141 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Hedera Proof of Action API 2 | 3 | Thank you for your interest in contributing the Hedera Proof of Action API! 4 | 5 | We appreciate your interest in helping us and the rest of our community. We welcome bug reports, feature requests, and code contributions. 6 | 7 | __Jump To:__ 8 | 9 | * [Bug Reports](#bug-reports) 10 | * [Feature Requests](#feature-requests) 11 | * [Code Contributions](#code-contributions) 12 | 13 | ## Bug Reports 14 | 15 | Bug reports are accepted through the [Issues][issues] page. 16 | 17 | The [bug][label-bug] label is used to track bugs. 18 | 19 | ### Before Submitting a Bug Report 20 | 21 | Before submitting a bug report, please do the following: 22 | 23 | 1. Do a search through the existing issues to make sure it has not already been reported. If you find that the bug has already been raised, please give it a +1 to help us to decide which issues we prioritise. 24 | 25 | 2. If possible, upgrade to the latest release of the SDK. It's possible the bug has already been fixed in the latest version. We will do our utmost to maintain backwards compatibility between patch version releases, so that you can be confident that your application will continue to work as expected with the newer version. 26 | 27 | If you have completed these steps and you need to submit a bug report, please read the guidelines below. 28 | 29 | ### Submitting a Bug Report 30 | 31 | Please ensure that your bug report contains the following: 32 | 33 | * A short, descriptive title. Other community members should be able to understand the nature of the issue by reading this title. 34 | * A succinct, detailed description of the problem you're experiencing. This should include: 35 | * Expected behaviour of the SDK and the actual behaviour exhibited. 36 | * Any details of your application development environment that may be relevant. 37 | * If applicable, the exception stack-trace. 38 | * If you are able to create one, include a [Minimal Working Example][mwe] that reproduces the issue. 39 | * [Markdown][markdown] formatting as appropriate to make the report easier to read; for example use code blocks when pasting a code snippet or exception stack-trace. 40 | 41 | ## Feature Requests 42 | 43 | Feature requests are also submitted through the [Issues][issues] page. 44 | 45 | As with Bug Reports, please do a search of the open requests first before submitting a new one to avoid duplicates. If you do find a a feature request that represents your suggestion, please give it a +1. 46 | 47 | __NOTE:__ If you intend to implement this feature, please submit the feature request *before* working on any code changes. This will allow members on the SDK team to assess the idea, discuss the design with you and ensure that it makes sense to include such a feature in the SDK. 48 | 49 | Feature requests are labeled as [enhancements][label-enhancement]. 50 | 51 | ### Submitting a Feature Request 52 | 53 | Open an [issue][issues] with the following: 54 | 55 | * A short, descriptive title. Other community members should be able to understand the nature of the issue by reading this title. 56 | * A detailed description of the the proposed feature. Explain why you believe it should be added to the SDK. Illustrative example code may also be provided to help explain how the feature should work. 57 | * [Markdown][markdown] formatting as appropriate to make the request easier to read. 58 | * If you plan to implement this feature yourself, please let us know that you'd like to the issue to be assigned to you. 59 | 60 | ## Code Contributions 61 | 62 | Code contributions to the SDK are handled using [Pull Requests][pull-requests]. Please keep the following in mind when considering a code contribution: 63 | 64 | * The SDK is released under the [Apache 2.0 License][license]. 65 | 66 | Any code you submit will be released under this license. 67 | 68 | * For anything other than small or quick changes, you should always start by reviewing the [Issues][issues] page to ensure that the nobody else is already working on the same issue. 69 | 70 | If you're working on a bug fix, check to see whether the bug has already been reported. If it has but no one is assigned to it, ask one of the maintainers to assign it to you before beginning work. If you're confident the bug hasn't been reported yet, create a new [Bug Report](#bug-reports) and ask us to assign it to you. 71 | 72 | If you are thinking about adding entirely new functionality, open a [Feature Request](#feature-requests) or get in touch with us on [Discord](discord) to ask for feedback first before beginning work; this is to ensure that nobody else is already working on the feature (or another similar feature) and to confirm that it makes sense for such functionality to be included in the SDK. 73 | * All code contributions must be accompanied with new or modified tests that verify that the code works as expected; i.e. that the issue has been fixed or that the functionality works as intended. 74 | 75 | ### Pull Request Readiness 76 | 77 | Before submitting your pull request, refer to the pull request readiness checklist below: 78 | 79 | * [ ] Includes tests to exercise the new behaviour 80 | * [ ] Code is documented, especially public and user-facing constructs 81 | * [ ] Local run of `mvn checkstyle:check` and `mvn spotbugs:check` succeed 82 | * [ ] Git commit message is detailed and includes context behind the change 83 | * [ ] If the change is related to an existing Bug Report or Feature Request, please include its issue number 84 | 85 | To contribute, please fork the GitHub repository and submit a pull request to the `master` branch. 86 | 87 | ### Getting Your Pull Request Merged 88 | 89 | All Pull Requests must be approved by at least one member of the SDK team before it can be merged in. The members only have limited bandwidth to review Pull Requests so it's not unusual for a Pull Request to go unreviewed for a few days, especially if it's a large or complex one. After a couple of weeks, if you haven't received any feedback regarding your Pull Request from the SDK team, feel free to contact us on [Discord](discord) to ask for a review. 90 | 91 | ## Getting in Contact 92 | 93 | * Join us in our [Discord][discord] channel; there you can engage with the Hedera team and other developers and enthusiasts. 94 | 95 | [license]: https://github.com/hashgraph/hedera-proof-of-action-api/blob/master/LICENSE 96 | [mwe]: https://en.wikipedia.org/wiki/Minimal_Working_Example 97 | [markdown]: https://guides.github.com/features/mastering-markdown/ 98 | [issues]: https://github.com/hashgraph/hedera-proof-of-action-api/issues 99 | [pull-requests]: https://github.com/hashgraph/hedera-proof-of-action-api/pulls 100 | [label-bug]: https://github.com/hashgraph/hedera-proof-of-action-api/labels/bug 101 | [label-enhancement]: https://github.com/hashgraph/hedera-proof-of-action-api/labels/enhancement 102 | [discord]: https://hedera.com/discord 103 | -------------------------------------------------------------------------------- /src/main/java/com/hedera/hashgraph/poa/App.java: -------------------------------------------------------------------------------- 1 | package com.hedera.hashgraph.poa; 2 | 3 | import com.google.common.flogger.FluentLogger; 4 | import com.hedera.hashgraph.sdk.AccountId; 5 | import com.hedera.hashgraph.sdk.Client; 6 | import com.hedera.hashgraph.sdk.HederaPreCheckStatusException; 7 | import com.hedera.hashgraph.sdk.HederaReceiptStatusException; 8 | import com.hedera.hashgraph.sdk.PrivateKey; 9 | import com.hedera.hashgraph.sdk.TopicCreateTransaction; 10 | import com.hedera.hashgraph.sdk.TopicId; 11 | import io.github.cdimascio.dotenv.Dotenv; 12 | import io.github.cdimascio.dotenv.DotenvBuilder; 13 | import io.vertx.core.AbstractVerticle; 14 | import io.vertx.core.Promise; 15 | import io.vertx.core.http.HttpServer; 16 | import io.vertx.ext.web.RoutingContext; 17 | import io.vertx.ext.web.handler.BodyHandler; 18 | import io.vertx.ext.web.handler.CorsHandler; 19 | import io.vertx.pgclient.PgConnectOptions; 20 | import io.vertx.pgclient.PgPool; 21 | import io.vertx.sqlclient.PoolOptions; 22 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 23 | import org.bouncycastle.util.encoders.Hex; 24 | import org.flywaydb.core.Flyway; 25 | 26 | import javax.crypto.SecretKey; 27 | import javax.crypto.spec.SecretKeySpec; 28 | import java.security.SecureRandom; 29 | import java.security.Security; 30 | import java.util.Objects; 31 | import java.util.concurrent.TimeoutException; 32 | 33 | import static io.vertx.core.Vertx.vertx; 34 | import static io.vertx.ext.web.Router.router; 35 | 36 | public class App extends AbstractVerticle { 37 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 38 | 39 | private static final Dotenv env = new DotenvBuilder().ignoreIfMissing().load(); 40 | 41 | private static SecretKey secretKey; 42 | 43 | private static final AccountId hederaOperatorId = AccountId.fromString( 44 | Objects.requireNonNull(env.get("HEDERA_OPERATOR_ID"))); 45 | 46 | private static final PrivateKey hederaOperatorKey = PrivateKey.fromString( 47 | Objects.requireNonNull(env.get("HEDERA_OPERATOR_KEY"))); 48 | 49 | private static TopicId hederaTopicId; 50 | 51 | // note: this does not connect until first use 52 | private static Client hederaClient; 53 | 54 | private HttpServer httpServer; 55 | 56 | public static void main(String[] args) throws TimeoutException, HederaPreCheckStatusException, HederaReceiptStatusException { 57 | Security.addProvider(new BouncyCastleProvider()); 58 | 59 | // generate the secret key and announce if we needed to 60 | 61 | var secretKeyText = env.get("SECRET_KEY"); 62 | if (secretKeyText == null || secretKeyText.isBlank()) { 63 | var random = new SecureRandom(); 64 | var keyBytes = new byte[32]; 65 | random.nextBytes(keyBytes); 66 | 67 | logger.atWarning().log("generated new encryption key: %s", new String(Hex.encode(keyBytes))); 68 | logger.atInfo().log("you must save and re-use this encryption key to decrypt any data transferred to Hedera"); 69 | 70 | secretKey = new SecretKeySpec(keyBytes, "AES"); 71 | } else { 72 | secretKey = new SecretKeySpec( 73 | Hex.decode(secretKeyText), "AES" 74 | ); 75 | } 76 | 77 | // load and run database migrations 78 | 79 | Flyway.configure() 80 | .dataSource("jdbc:" + Objects.requireNonNull(env.get("DATABASE_URL")), env.get("DATABASE_USER"), env.get("DATABASE_PASSWORD")) 81 | .load() 82 | .migrate(); 83 | 84 | setupClient(); 85 | 86 | // announce the topic ID we'll be using and/or create the topic ID if one was not provided 87 | 88 | var hederaTopicIdText = env.get("HEDERA_TOPIC_ID"); 89 | if ((hederaTopicIdText == null) || (hederaTopicIdText.isEmpty())) { 90 | // make a new topic ID 91 | var transactionId = new TopicCreateTransaction() 92 | .setSubmitKey(hederaOperatorKey) 93 | .execute(hederaClient) 94 | .transactionId; 95 | 96 | hederaTopicId = transactionId 97 | .getReceipt(hederaClient) 98 | .topicId; 99 | 100 | logger.atWarning().log("created new topic ID %s", hederaTopicId); 101 | } else { 102 | hederaTopicId = TopicId.fromString(hederaTopicIdText); 103 | } 104 | 105 | vertx().deployVerticle(new App()); 106 | } 107 | 108 | private static void setupClient() { 109 | // setup the Hedera client from .env 110 | var network = env.get("HEDERA_NETWORK"); 111 | 112 | if ((network == null) || (network.isBlank())) { 113 | logger.atSevere().log("HEDERA_NETWORK environment variable not set - exiting"); 114 | System.exit(1); 115 | } 116 | switch (network.toLowerCase()) { 117 | case "testnet": 118 | hederaClient = Client.forTestnet(); 119 | break; 120 | case "mainnet": 121 | hederaClient = Client.forMainnet(); 122 | break; 123 | // case "previewnet": 124 | // hederaClient = Client.forPreviewNet(); 125 | // break; 126 | } 127 | hederaClient.setOperator(hederaOperatorId, hederaOperatorKey); 128 | } 129 | 130 | @Override 131 | public void start(Promise startPromise) { 132 | // create the database pool 133 | var options = PgConnectOptions.fromUri(env.get("DATABASE_URL")) 134 | .setUser(env.get("DATABASE_USER")) 135 | .setPassword(env.get("DATABASE_PASSWORD")); 136 | 137 | var db = PgPool.pool(vertx, options, new PoolOptions()); 138 | 139 | // saves the server so we can cleanly exit later 140 | httpServer = vertx.createHttpServer(); 141 | 142 | var router = router(vertx); 143 | router.errorHandler(500, this::handleError); 144 | 145 | router.route() 146 | // handle accepting request bodies but cap it at 4 Ki 147 | .handler(BodyHandler.create().setBodyLimit(4096)) 148 | // let anyone in, it's a free world 149 | .handler(CorsHandler.create("*")); 150 | 151 | router.post("/v1/action").handler(new CreateActionHandler( 152 | db, 153 | hederaClient, 154 | hederaOperatorId, 155 | hederaTopicId, 156 | secretKey 157 | )); 158 | 159 | var searchHandler = new SearchActionHandler(db); 160 | 161 | router.get("/v1/action").handler(searchHandler); 162 | router.post("/v1/action/search").handler(searchHandler); 163 | 164 | httpServer.requestHandler(router); 165 | httpServer.listen(8080, "0.0.0.0", v -> { 166 | if (v.succeeded()) { 167 | logger.atInfo().log("listening on http://0.0.0.0:8080/"); 168 | 169 | startPromise.complete(); 170 | } else { 171 | startPromise.fail(v.cause()); 172 | } 173 | }); 174 | } 175 | 176 | @Override 177 | public void stop(Promise stopPromise) { 178 | hederaClient.close(); 179 | httpServer.close(stopPromise); 180 | } 181 | 182 | private void handleError(RoutingContext rx) { 183 | logger.atSevere().withCause(rx.failure()).log(); 184 | rx.response().setStatusCode(500).end(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/com/hedera/hashgraph/poa/CreateActionHandler.java: -------------------------------------------------------------------------------- 1 | package com.hedera.hashgraph.poa; 2 | 3 | import com.google.common.flogger.FluentLogger; 4 | import com.google.common.hash.Hashing; 5 | import com.hedera.hashgraph.sdk.AccountId; 6 | import com.hedera.hashgraph.sdk.Client; 7 | import com.hedera.hashgraph.sdk.TopicMessageSubmitTransaction; 8 | import com.hedera.hashgraph.sdk.TopicId; 9 | import com.hedera.hashgraph.sdk.TransactionId; 10 | import io.vertx.core.Handler; 11 | import io.vertx.core.buffer.Buffer; 12 | import io.vertx.core.json.Json; 13 | import io.vertx.ext.web.RoutingContext; 14 | import io.vertx.pgclient.PgPool; 15 | import io.vertx.sqlclient.Tuple; 16 | import java.util.concurrent.CompletableFuture; 17 | 18 | import javax.crypto.BadPaddingException; 19 | import javax.crypto.Cipher; 20 | import javax.crypto.IllegalBlockSizeException; 21 | import javax.crypto.NoSuchPaddingException; 22 | import javax.crypto.SecretKey; 23 | import java.security.InvalidKeyException; 24 | import java.security.NoSuchAlgorithmException; 25 | import java.security.NoSuchProviderException; 26 | import java.util.Objects; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | import static java.util.concurrent.CompletableFuture.delayedExecutor; 30 | 31 | public class CreateActionHandler implements Handler { 32 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 33 | 34 | private final PgPool db; 35 | 36 | private final Client hedera; 37 | 38 | private final AccountId hederaOperatorId; 39 | 40 | private final TopicId hederaTopicId; 41 | 42 | private final SecretKey encryptionSecretKey; 43 | 44 | public CreateActionHandler(PgPool db, Client hedera, AccountId hederaOperatorId, TopicId hederaTopicId, SecretKey encryptionSecretKey) { 45 | this.db = db; 46 | this.hedera = hedera; 47 | this.hederaTopicId = hederaTopicId; 48 | this.hederaOperatorId = hederaOperatorId; 49 | this.encryptionSecretKey = encryptionSecretKey; 50 | } 51 | 52 | @Override 53 | public void handle(RoutingContext rx) { 54 | var res = rx.response(); 55 | 56 | var reqBody = rx.getBody(); 57 | if (reqBody.length() == 0) { 58 | // no content, instant failure 59 | res.setStatusCode(400); 60 | res.end(); 61 | return; 62 | } 63 | 64 | var req = Json.decodeValue(rx.getBody(), CreateActionRequest.class); 65 | 66 | // pre-generate the transaction ID so we can save the record of the action 67 | var transactionId = TransactionId.generate(hederaOperatorId); 68 | 69 | // create a future to receive the action ID 70 | var actionIdFut = new CompletableFuture(); 71 | 72 | // prepare the message to submit to hedera 73 | byte[] messageToSubmit = req.payload.getBytes(); 74 | 75 | switch (req.submit) { 76 | case DIRECT: 77 | break; 78 | 79 | case HASH: 80 | // noinspection UnstableApiUsage 81 | messageToSubmit = Hashing.sha384().hashBytes(messageToSubmit).toString().getBytes(); 82 | break; 83 | 84 | case ENCRYPTED: 85 | Cipher cipher = null; 86 | 87 | try { 88 | cipher = Cipher.getInstance("AES/ECB/PKCS7Padding", "BC"); 89 | } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) { 90 | throw new RuntimeException(e); 91 | } 92 | 93 | try { 94 | cipher.init(Cipher.ENCRYPT_MODE, encryptionSecretKey); 95 | } catch (InvalidKeyException e) { 96 | throw new RuntimeException(e); 97 | } 98 | 99 | try { 100 | messageToSubmit = cipher.doFinal(messageToSubmit); 101 | } catch (IllegalBlockSizeException | BadPaddingException e) { 102 | throw new RuntimeException(e); 103 | } 104 | 105 | break; 106 | } 107 | 108 | // submit to HCS 109 | // note: this intentionally does not block the HTTP request from progressing, we want to immediately return 110 | // to the client here 111 | new TopicMessageSubmitTransaction() 112 | .setTransactionId(transactionId) 113 | .setMessage(messageToSubmit) 114 | .setTopicId(hederaTopicId) 115 | .executeAsync(hedera) 116 | // note: futures flow so nicely, this is almost sync. level clarity and its async execution here 117 | .thenComposeAsync(id -> id.transactionId.getRecordAsync(hedera), delayedExecutor(5, TimeUnit.SECONDS)) 118 | .thenCombineAsync(actionIdFut, (record, actionId) -> { 119 | var fut = new CompletableFuture(); 120 | 121 | var params = Tuple.of( 122 | actionId, 123 | InstantConverter.toNanos(record.consensusTimestamp), 124 | record.receipt.topicSequenceNumber, 125 | Buffer.buffer(Objects.requireNonNull(record.receipt.topicRunningHash).toByteArray()), 126 | Buffer.buffer(record.transactionHash.toByteArray()) 127 | ); 128 | 129 | db.preparedQuery( 130 | "INSERT INTO proofs ( action_id, consensus_timestamp, sequence_number, running_hash, transaction_hash ) " + 131 | "VALUES ( $1, $2, $3, $4, $5 )" 132 | ).execute(params, v -> { 133 | if (v.failed()) { 134 | fut.completeExceptionally(v.cause()); 135 | return; 136 | } 137 | 138 | // language=text 139 | fut.complete(null); 140 | }); 141 | 142 | return fut; 143 | }) 144 | .whenComplete((v, error) -> { 145 | if (error != null) { 146 | // failed to submit the message 147 | logger.atSevere().withCause(error).log(); 148 | 149 | // FIXME: as this is currently in a proof-of-concept stage, we're ignoring the potential failure 150 | // in a real application, we would want to retry or something 151 | } 152 | }); 153 | 154 | db.preparedQuery( 155 | "INSERT INTO actions ( payload, transaction_id_num, transaction_id_valid_start, client_id ) " + 156 | "VALUES ( $1, $2, $3, $4 ) " + 157 | "RETURNING id" 158 | ).execute(Tuple.of(req.payload, transactionId.accountId.num, InstantConverter.toNanos(transactionId.validStart), req.clientId), v -> { 159 | if (v.failed()) { 160 | rx.fail(v.cause()); 161 | return; 162 | } 163 | 164 | // pull out the generated action ID 165 | var row = v.result().iterator().next(); 166 | var actionId = row.getLong(0); 167 | 168 | // and pass the action ID to our other future 169 | actionIdFut.complete(actionId); 170 | 171 | try { 172 | // 202 -> ACCEPTED 173 | // the idea is we record the action but we are pending on the proof 174 | res.setStatusCode(202) 175 | .putHeader("content-type", "application/json") 176 | .end(Json.encode(new CreateActionResponse(transactionId))); 177 | } catch (Exception e) { 178 | e.printStackTrace(); 179 | } 180 | }); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Hedera Hashgraph LLC 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------