diff --git a/cip/cip30/src/main/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSigner.java b/cip/cip30/src/main/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSigner.java index a386c7cd..a24ece9a 100644 --- a/cip/cip30/src/main/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSigner.java +++ b/cip/cip30/src/main/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSigner.java @@ -1,6 +1,7 @@ package com.bloxbean.cardano.client.cip.cip30; import co.nstant.in.cbor.model.ByteString; +import co.nstant.in.cbor.model.SimpleValue; import co.nstant.in.cbor.model.UnsignedInteger; import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.Address; @@ -17,17 +18,18 @@ * CIP30 signData() implementation to create and verify signature */ public enum CIP30DataSigner { + INSTANCE(); CIP30DataSigner() { - } /** * Sign and create DataSignature in CIP30's signData() format + * * @param addressBytes Address bytes - * @param payload payload bytes to sign - * @param signer signing account + * @param payload payload bytes to sign + * @param signer signing account * @return DataSignature * @throws DataSignError */ @@ -36,7 +38,39 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl byte[] pvtKey = signer.privateKeyBytes(); byte[] pubKey = signer.publicKeyBytes(); - return signData(addressBytes, payload, pvtKey, pubKey); + return signData(addressBytes, payload, pvtKey, pubKey, false); + } + + /** + * Sign and create DataSignature in CIP30's signData() format + * + * @param addressBytes Address bytes + * @param payload payload bytes to sign + * @param signer signing account + * @param hashPayload indicates if the payload is expected to be hashed + * @return DataSignature + * @throws DataSignError + */ + public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull Account signer, boolean hashPayload) + throws DataSignError { + byte[] pvtKey = signer.privateKeyBytes(); + byte[] pubKey = signer.publicKeyBytes(); + + return signData(addressBytes, payload, pvtKey, pubKey, hashPayload); + } + + /** + * Sign and create DataSignature in CIP30's signData() format + * + * @param addressBytes Address bytes + * @param payload payload bytes to sign + * @param pvtKey private key bytes + * @param pubKey public key bytes to add + * @return DataSignature + * @throws DataSignError + */ + public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull byte[] pvtKey, @NonNull byte[] pubKey) throws DataSignError { + return signData(addressBytes, payload, pvtKey, pubKey, false); } /** @@ -45,10 +79,11 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl * @param payload payload bytes to sign * @param pvtKey private key bytes * @param pubKey public key bytes to add + * @param hashPayload indicates if the payload is expected to be hashed * @return DataSignature * @throws DataSignError */ - public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull byte[] pvtKey, @NonNull byte[] pubKey) + public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull byte[] pvtKey, @NonNull byte[] pubKey, boolean hashPayload) throws DataSignError { try { HeaderMap protectedHeaderMap = new HeaderMap() @@ -60,12 +95,11 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl ._protected(new ProtectedHeaderMap(protectedHeaderMap)) .unprotected(new HeaderMap()); - COSESign1Builder coseSign1Builder = new COSESign1Builder(headers, payload, false); + COSESign1Builder coseSign1Builder = new COSESign1Builder(headers, payload, false).hashed(hashPayload); SigStructure sigStructure = coseSign1Builder.makeDataToSign(); byte[] signature; - if (pvtKey.length >= 64) { //64 bytes expanded pvt key signature = Configuration.INSTANCE.getSigningProvider().signExtended(sigStructure.serializeAsBytes(), pvtKey); } else { //32 bytes pvt key @@ -74,7 +108,6 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl COSESign1 coseSign1 = coseSign1Builder.build(signature); - //COSEKey COSEKey coseKey = new COSEKey() .keyType(OKP) //OKP .keyId(addressBytes) @@ -82,8 +115,10 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl .addOtherHeader(CRV_KEY, new UnsignedInteger(CRV_Ed25519)) //crv Ed25519 .addOtherHeader(X_KEY, new ByteString(pubKey)); //x pub key used to sign sig_structure - return new DataSignature(HexUtil.encodeHexString(coseSign1.serializeAsBytes()), - HexUtil.encodeHexString(coseKey.serializeAsBytes())); + String sig = HexUtil.encodeHexString(coseSign1.serializeAsBytes()); + String key = HexUtil.encodeHexString(coseKey.serializeAsBytes()); + + return new DataSignature(sig, key); } catch (Exception e) { throw new DataSignError("Error signing data", e); } @@ -91,6 +126,7 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl /** * Verify CIP30 signData signature + * * @param dataSignature * @return true if verification is successful, otherwise false */ @@ -106,8 +142,9 @@ public boolean verify(@NonNull DataSignature dataSignature) { .verify(signature, sigStructure.serializeAsBytes(), pubKey); //Verify address - byte[] addressBytes = coseSign1.headers()._protected().getAsHeaderMap().otherHeaderAsBytes(ADDRESS_KEY); + byte[] addressBytes = coseSign1.headers()._protected().getAsHeaderMap().otherHeaderAsBytes(ADDRESS_KEY); Address address = new Address(addressBytes); + boolean addressVerified = AddressProvider.verifyAddress(address, pubKey); return sigVerified && addressVerified; diff --git a/cip/cip30/src/test/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSignerTest.java b/cip/cip30/src/test/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSignerTest.java index 4a2ebee8..4849801b 100644 --- a/cip/cip30/src/test/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSignerTest.java +++ b/cip/cip30/src/test/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSignerTest.java @@ -6,13 +6,16 @@ import com.bloxbean.cardano.client.address.Address; import com.bloxbean.cardano.client.cip.cip8.COSEKey; import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.Blake2bUtil; import com.bloxbean.cardano.client.util.HexUtil; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class CIP30DataSignerTest { + String mnemonic = "nice orient enjoy teach jump office alert inquiry apart unaware seat tumble unveil device have bullet morning eyebrow time image embody divide version uniform"; + Account account = new Account(Networks.testnet(), mnemonic); @Test @@ -68,5 +71,50 @@ void verifyNamiSignature_invalidKey() { assertThat(verified).isFalse(); } -} + @Test + void verifyHashedLedgerHardwareWallet() { + DataSignature dataSignature = new DataSignature() + .signature("84582aa201276761646472657373581de103d205532089ad2f7816892e2ef42849b7b52788e41b3fd43a6e01cfa166686173686564f5581c1c1afc33a1ed48205eadcbbda2fc8e61442af2e04673616f21b7d0385840954858f672e9ca51975655452d79a8f106011e9535a2ebfb909f7bbcce5d10d246ae62df2da3a7790edd8f93723cbdfdffc5341d08135b1a40e7a998e8b2ed06") + .key("a4010103272006215820c13745be35c2dfc3fa9523140030dda5b5346634e405662b1aae5c61389c55b3"); + boolean verified = CIP30DataSigner.INSTANCE.verify(dataSignature); + + assertThat(verified).isTrue(); + } + + @Test + void verifySignDataHashedPayload() { + DataSignature dataSignature = new DataSignature() + .signature("845846a2012767616464726573735839003175d03902583e82037438cc86732f6e539f803f9a8b2d4ee164b9d0c77e617030631811f60a1f8a8be26d65a57ff71825b336cc6b76361da166686173686564f44b48656c6c6f20576f726c64584036c2151e1230364b0bf9e40cb65dbdca4c5decf4187e3c5511945d410ea59a1e733b5e68178c234979053ed75b0226ba826fb951c5a79fabf10bddcabda8dc05") + .key("a4010103272006215820a5f73966e73d0bb9eadc75c5857eafd054a0202d716ac6dde00303ee9c0019e3"); + + boolean verified = CIP30DataSigner.INSTANCE.verify(dataSignature); + assertThat(verified).isTrue(); + } + + @Test + void signDataHashedPayload() throws DataSignError { + byte[] payload = "Hello World".getBytes(); + + Address address = new Address(account.baseAddress()); + DataSignature dataSignature = CIP30DataSigner.INSTANCE.signData(address.getBytes(), payload, account, true); + + assertThat(dataSignature).isNotNull(); + assertThat(dataSignature.signature()).isEqualTo("845882a3012704583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e6761646472657373583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0ea166686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d5840d6348538f8c69f5ac30615700b78597dc29795d5fef2aa6165f17ac208b3163b2d2d55405beb6cd8fc66e3beaac1d08b91fae7b9679cc0ae212c65cfe277d608"); + assertThat(dataSignature.key()).isEqualTo("a5010102583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e03272006215820097c8507b71063f99e38147f09eacf76f25576a2ddfac2f40da8feee8dab2d5d"); + assertThat(HexUtil.encodeHexString(dataSignature.address())).isEqualTo("00327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e"); + } + + @Test + public void verifySignedHashedPayload() { + String sig = "845882a3012704583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e6761646472657373583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0ea166686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d5840d6348538f8c69f5ac30615700b78597dc29795d5fef2aa6165f17ac208b3163b2d2d55405beb6cd8fc66e3beaac1d08b91fae7b9679cc0ae212c65cfe277d608"; + String key = "a5010102583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e03272006215820097c8507b71063f99e38147f09eacf76f25576a2ddfac2f40da8feee8dab2d5d"; + + DataSignature dataSig = new DataSignature().signature(sig).key(key); + + boolean isVerified = CIP30DataSigner.INSTANCE.verify(dataSig); + + assertThat(isVerified).isTrue(); + } + +} diff --git a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/COSESign1.java b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/COSESign1.java index 55df9bfe..8e64721f 100644 --- a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/COSESign1.java +++ b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/COSESign1.java @@ -113,3 +113,4 @@ public SigStructure signedData(byte[] externalAad, byte[] externalPayload) { .externalAad(externalAad); } } + diff --git a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/ProtectedHeaderMap.java b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/ProtectedHeaderMap.java index fe666a6f..062813c9 100644 --- a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/ProtectedHeaderMap.java +++ b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/ProtectedHeaderMap.java @@ -33,7 +33,7 @@ public static ProtectedHeaderMap deserialize(DataItem dataItem) { } else { throw new CborRuntimeException( String.format("Deserialization error: Invalid type for ProtectedHeaderMap, type: %s, " + - "expected type: ByteString" + dataItem.getMajorType())); + "expected type: ByteString", dataItem.getMajorType())); } } diff --git a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1Builder.java b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1Builder.java index 78910880..688c320e 100644 --- a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1Builder.java +++ b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1Builder.java @@ -26,12 +26,20 @@ public COSESign1Builder(Headers headers, byte[] payload, boolean isPayloadExtern public SigStructure makeDataToSign() { Headers headersCopy = headers.copy(); + headersCopy.unprotected().addOtherHeader("hashed", hashed ? SimpleValue.TRUE : SimpleValue.FALSE); + + byte[] finalPayload; + if (isPayloadExternal) { + finalPayload = payload.clone(); + } else { + finalPayload = hashed ? Blake2bUtil.blake2bHash224(payload): payload.clone(); + } return new SigStructure() .sigContext(SigContext.Signature1) .bodyProtected(headersCopy._protected()) .externalAad(externalAad != null ? externalAad.clone() : new byte[0]) - .payload(payload.clone()); + .payload(finalPayload); } public COSESign1 build(byte[] signedSigStructure) { diff --git a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilder.java b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilder.java index 171aa046..ce6f0609 100644 --- a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilder.java +++ b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilder.java @@ -26,11 +26,17 @@ public COSESignBuilder(Headers headers, byte[] payload, boolean isPayloadExterna public SigStructure makeDataToSign() { Headers headersCopy = headers.copy(); + byte[] finalPayload; + if (isPayloadExternal) { + finalPayload = payload.clone(); + } else + finalPayload = hashed? Blake2bUtil.blake2bHash224(payload): payload.clone(); + return new SigStructure() .sigContext(SigContext.Signature) .bodyProtected(headersCopy._protected()) .externalAad(externalAad != null ? externalAad.clone() : new byte[0]) - .payload(payload.clone()); + .payload(finalPayload); } public COSESign build(List coseSignatures) { diff --git a/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1BuilderTest.java b/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1BuilderTest.java index bce01503..959f543e 100644 --- a/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1BuilderTest.java +++ b/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1BuilderTest.java @@ -35,6 +35,7 @@ void buildCOSESign1() throws CborException { .unprotected(unpHeadermap); byte[] payload = "Hello World".getBytes(); + System.out.println("Payload: " + HexUtil.encodeHexString(payload)); COSESign1Builder coseSign1Builder = new COSESign1Builder(headers, payload, false) .hashed(true); @@ -45,12 +46,14 @@ void buildCOSESign1() throws CborException { COSESign1 coseSign1 = coseSign1Builder.build(signedSigStructure); String serHex = HexUtil.encodeHexString(coseSign1.serializeAsBytes()); - System.out.println(serHex); - //This hex is the result from message-signing rust impl. - String expected = "8447a2010e033903e7a2386371536f6d65206865616465722076616c756566686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d58400a448415208ba496d5cd58407a05269b8f0fd14a3c690b761b03c58e2ac70dd36a6bb9d0e03c5baa9d68da99af4be2a8245892325535ec3656435505ba182703"; + //This hex is the result from message-signing rust impl. (Check cose_sign1_builder.rs) + String expected = "8447a2010e033903e7a2386371536f6d65206865616465722076616c756566686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d58400a810f4fef824d98bb3d08a93f32b2bffb236ecc87100142911605509b953701b0680ce347a13d54e6f626c1f368e69e422d75870db21f8c8ad9f1e40f51ca04"; COSESign1 coseSign12 = COSESign1.deserialize(CborDecoder.decode(HexUtil.decodeHexString(serHex)).get(0)); + System.out.println("Serialized Hex: " + serHex.length()); + System.out.println("Expected Hex: " + expected.length()); + assertThat(serHex).isEqualTo(expected); assertThat(coseSign12).isEqualTo(coseSign1); } @@ -130,7 +133,6 @@ void buildCOSESign1_withPayLoadExTrue_additionalHeaders() throws CborException { COSESign1 coseSign1 = coseSign1Builder.build(signedSigStructure); String serHex = HexUtil.encodeHexString(coseSign1.serializeAsBytes()); - System.out.println(serHex); //This hex is the result from message-signing rust impl. String expected = "8458baa7010e02816d637269746963616c6974792d31033903e704430102030543040506064304050607828340a10281055840030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303038340a20103028108584005050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505aa01181802816d637269746963616c6974792d32033907cf04430102030543040506064304050607828340a10281145840030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303038340a20103028108584005050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505386371736f6d65206865616465722076616c7565646b6579314307080966686173686564f5f65840ba0e7cb56486b33c1fc3fb2730968cf46b9215bcf57dfdec15102cad72391b4ac2f5c6a23fe3e2545b6d0d2381fc5fbb090467e02f74d57eee8380b8cf9d1605"; diff --git a/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilderTest.java b/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilderTest.java index 66f385ff..26499b1c 100644 --- a/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilderTest.java +++ b/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilderTest.java @@ -9,6 +9,7 @@ import com.bloxbean.cardano.client.cip.cip8.*; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.config.Configuration; +import com.bloxbean.cardano.client.crypto.Blake2bUtil; import com.bloxbean.cardano.client.crypto.api.SigningProvider; import com.bloxbean.cardano.client.util.HexUtil; import org.junit.jupiter.api.Test; @@ -45,12 +46,14 @@ void buildCOSESign() throws CborException { String serHex = HexUtil.encodeHexString(coseSign.serializeAsBytes()); System.out.println(serHex); - //This hex is the result from message-signing rust impl. - String expected = "8447a2010e033903e7a2386371536f6d65206865616465722076616c756566686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d828340a238c77819616e6f74686572206164646974696f6e616c20686561646572646b6579316a6b6579312076616c756558408a991fa149aa4ac06cfea4a36f798b06e86cd7231e5dada423893a302de2e7278b7589de9bada77a8e597c5a1916d28787a052f5f19c510c980faae1d4e909078340a239018f781a616e6f74686572206164646974696f6e616c2068656164657232646b6579326a6b6579322076616c7565584093279db72ff01b677f4cdef33fefc0ac48932b5f15e4eb2427553e83127cc3bac4e7b459ff3c39b0d874c22c5250130aba15e3981eccfc0a2f58f53dcb06a90e"; - COSESign coseSign2 = COSESign.deserialize(CborDecoder.decode(HexUtil.decodeHexString(serHex)).get(0)); + //This hex is the result from message-signing rust impl. (Check cose_sign_builder.rs) + String expected = "8447a2010e033903e7a1386371536f6d65206865616465722076616c7565581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d828340a238c77819616e6f74686572206164646974696f6e616c20686561646572646b6579316a6b6579312076616c7565584098b74a575e435c5506ec80bc4b47aceba462a4edaf785c345c022acb80957ddbdb36177f3a95cee97efdb474bbdcb66db0fe93e9b011523a8a36d8b443dbb5008340a239018f781a616e6f74686572206164646974696f6e616c2068656164657232646b6579326a6b6579322076616c756558406161857b10b1bfa62bdf6f3ae9d751cc361446af41ec79fa2fca8fe67d27f3d8622ad99786539aa3dedd4d7456d5e13d5474f3d72babd37f6dbe09bfc8c12701"; + COSESign expectedCoseSign2 = COSESign.deserialize(CborDecoder.decode(HexUtil.decodeHexString(expected)).get(0)); - assertThat(serHex).isEqualTo(expected); - assertThat(coseSign2).isEqualTo(coseSign); + //rust message-signing lib doesn't add hashed key to the unprotected headers. So, we are just checking the signatures here + //But rust COSESign1Builder adds hashed key to the unprotected headers. Not sure why? + assertThat(coseSign.signatures().get(0).signature()).endsWith(expectedCoseSign2.signatures().get(0).signature()); + assertThat(coseSign.signatures().get(1).signature()).endsWith(expectedCoseSign2.signatures().get(1).signature()); } @Test diff --git a/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/cose_sign1_builder.rs b/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/cose_sign1_builder.rs new file mode 100644 index 00000000..a0a1946b --- /dev/null +++ b/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/cose_sign1_builder.rs @@ -0,0 +1,39 @@ +//Using https://github.com/Emurgo/message-signing +//This is just here for reference. So that we can compare the output of the rust code with the output of the java code. +use cardano_message_signing as ms; +use cardano_message_signing::builders::COSESign1Builder; +use cardano_message_signing::cbor::CBORValue; +use cardano_message_signing::utils::{Int, ToBytes}; +use cardano_message_signing::{HeaderMap, Headers, Label, ProtectedHeaderMap}; +use cardano_serialization_lib as csl; + +fn main() { + use ms::utils::ToBytes; + let pvt_key_hex = "a09afd74a50ea23fe8607f71766cd56fd78e44e4ee7563e53a690de1d74da15e41802f82cfe0718ade23369800857b885abc1c512aa43059be6e4ce2e8f43ce73d16a13b6998d256de5835612f44fdd56e2f24ab2694b7b70c68f8fd77325c83"; + let sk_bytes = hex::decode(pvt_key_hex).unwrap(); + + let sk = csl::crypto::Bip32PrivateKey::from_bytes(&sk_bytes).unwrap(); + let pk = sk.to_public(); + let mut headerMap = HeaderMap::new(); + headerMap.set_algorithm_id(&Label::new_int(&Int::new_i32(14))); + headerMap.set_content_type(&Label::new_int(&Int::new_i32(-1000))); + let protected = ProtectedHeaderMap::new(&headerMap); + + let mut unprotected = HeaderMap::new(); + unprotected.set_header(&Label::new_int(&Int::new_i32(-100)), &CBORValue::new_text(String::from("Some header value"))); + let headers = Headers::new(&protected, &unprotected); + + let payload = String::from("Hello World").into_bytes(); + + let mut builder = COSESign1Builder::new(&headers, payload, false); + builder.hash_payload(); + + let sig_structure = builder.make_data_to_sign(); + + let signed_sig_struct = sk.to_raw_key().sign(&sig_structure.to_bytes()).to_bytes(); + let cose_sign1 = builder.build(signed_sig_struct); + + let serialized = cose_sign1.to_bytes(); + let hex = hex::encode(serialized.clone()); + println!("serialized = {:?}", hex); +} diff --git a/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/cose_sign_builder.rs b/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/cose_sign_builder.rs new file mode 100644 index 00000000..6bdcd288 --- /dev/null +++ b/cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/cose_sign_builder.rs @@ -0,0 +1,74 @@ +//Using https://github.com/Emurgo/message-signing +//This is just here for reference. So that we can compare the output of the rust code with the output of the java code. +use cardano_message_signing as ms; +use cardano_serialization_lib as csl; +use cardano_message_signing::{COSESignature, COSESignatures, HeaderMap, Headers, Label, ProtectedHeaderMap}; +use cardano_message_signing::builders::{COSESign1Builder, COSESignBuilder}; +use cardano_message_signing::cbor::CBORValue; +use cardano_message_signing::utils::{Int, ToBytes}; + +fn main() { + use ms::utils::ToBytes; + //Mnemonic: "nice orient enjoy teach jump office alert inquiry apart unaware seat tumble unveil device have bullet morning eyebrow time image embody divide version uniform" + let pvt_key_hex1 = "a09afd74a50ea23fe8607f71766cd56fd78e44e4ee7563e53a690de1d74da15e41802f82cfe0718ade23369800857b885abc1c512aa43059be6e4ce2e8f43ce73d16a13b6998d256de5835612f44fdd56e2f24ab2694b7b70c68f8fd77325c83"; + let sk_bytes1 = hex::decode(pvt_key_hex1).unwrap(); + let bip32SK1 = csl::crypto::Bip32PrivateKey::from_bytes(&sk_bytes1).unwrap(); + + //Mnemonic: ""carbon time empty obey bicycle choice mind kitchen shadow call strike skull check flag series deal garlic wing uphold problem bamboo winner install price" + let pvt_key_hex2 = "a0bcf0bee440970644e0f7850776c38040e6be001a8847f07b2cc1f982153b55cf1b9e9c1ed5881bcb1546e1176e4852c41df5f72ccc876dcc5e227328e103a64d992baad3ce8e9b4d97541423535db5f82edf5b19e3a2ed602466824cfae9ce"; + let sk_bytes2 = hex::decode(pvt_key_hex2).unwrap(); + let bip32SK2 = csl::crypto::Bip32PrivateKey::from_bytes(&sk_bytes2).unwrap(); + + let mut headerMap = HeaderMap::new(); + headerMap.set_algorithm_id(&Label::new_int(&Int::new_i32(14))); + headerMap.set_content_type(&Label::new_int(&Int::new_i32(-1000))); + let protected = ProtectedHeaderMap::new(&headerMap); + + let mut unprotected = HeaderMap::new(); + unprotected.set_header(&Label::new_int(&Int::new_i32(-100)), &CBORValue::new_text(String::from("Some header value"))); + let headers = Headers::new(&protected, &unprotected); + + let payload = String::from("Hello World").into_bytes(); + + let mut builder = COSESignBuilder::new(&headers, payload, false); + builder.hash_payload(); + + //Build sig structure + let sig_structure = builder.make_data_to_sign(); + + //cose_signature1 + let sk1 = bip32SK1.to_raw_key(); + let signature1 = sk1.sign(&sig_structure.to_bytes()).to_bytes(); + + let mut cose_sig_unprotected_header1 = HeaderMap::new(); + cose_sig_unprotected_header1.set_header(&Label::new_int(&Int::new_i32(-200)), &CBORValue::new_text(String::from("another additional header"))); + cose_sig_unprotected_header1.set_header(&Label::new_text(String::from("key1")), &CBORValue::new_text(String::from("key1 value"))); + + let cose_sig_header1 = Headers::new(&ProtectedHeaderMap::new(&HeaderMap::new()), &cose_sig_unprotected_header1); + + let coseSignature1 = COSESignature::new(&cose_sig_header1, signature1); + + //cose_signature2 + let sk2 = bip32SK2.to_raw_key(); + let signature2 = sk2.sign(&sig_structure.to_bytes()).to_bytes(); + + let mut cose_sig_unprotected_header2 = HeaderMap::new(); + cose_sig_unprotected_header2.set_header(&Label::new_int(&Int::new_i32(-400)), &CBORValue::new_text(String::from("another additional header2"))); + cose_sig_unprotected_header2.set_header(&Label::new_text(String::from("key2")), &CBORValue::new_text(String::from("key2 value"))); + + let cose_sig_header2 = Headers::new(&ProtectedHeaderMap::new(&HeaderMap::new()), &cose_sig_unprotected_header2); + + let coseSignature2 = COSESignature::new(&cose_sig_header2, signature2); + + //Build cose_signatures + let mut cose_signatures = COSESignatures::new(); + cose_signatures.add(&coseSignature1); + cose_signatures.add(&coseSignature2); + + //Build cose_sign + let cose_sign = builder.build(&cose_signatures); + + let serialized = cose_sign.to_bytes(); + let hex = hex::encode(serialized.clone()); + println!("serialized = {:?}", hex); +} diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java index 142ab2ce..e4922a3b 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java @@ -32,6 +32,8 @@ import java.time.Duration; import java.time.Instant; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -539,6 +541,99 @@ public Result completeAndWait(@NonNull Duration timeout, @NonNull Durati return result; } + /** + * Completes the task and waits asynchronously with a specified timeout duration and a logging function. + * + * @return a CompletableFuture containing the result of the completion task. + */ + public CompletableFuture> completeAndWaitAsync() { + return completeAndWaitAsync(Duration.ofSeconds(2), (msg) -> log.info(msg)); + } + + /** + * Submit the transaction and return a CompletableFuture containing a Result that wraps a txHash if the operation is successful. + * + * @param logConsumer a consumer that processes log messages. It must not be null. + * @return a CompletableFuture containing a Result that wraps txHash if the operation is successful. + */ + public CompletableFuture> completeAndWaitAsync(@NonNull Consumer logConsumer) { + return completeAndWaitAsync(Duration.ofSeconds(2), logConsumer); + } + + /** + * Submit the transaction and return a CompletableFuture containing a Result that wraps a txHash if the operation is successful. + * + * @param executor the executor to use for asynchronous execution. It must not be null. + * @return + */ + public CompletableFuture> completeAndWaitAsync(@NonNull Executor executor) { + return completeAndWaitAsync(Duration.ofSeconds(2), (msg) -> log.info(msg), executor); + } + + /** + * Submit the transaction and return a CompletableFuture containing a Result that wraps a txHash if the operation is successful. + * + * @param logConsumer a consumer that processes log messages. It must not be null. + * @param executor the executor to use for asynchronous execution. It must not be null. + * @return a CompletableFuture containing a Result that wraps txHash if the operation is successful. + */ + public CompletableFuture> completeAndWaitAsync(@NonNull Consumer logConsumer, @NonNull Executor executor) { + return completeAndWaitAsync(Duration.ofSeconds(2), logConsumer, executor); + } + + /** + * Submit the transaction and return a CompletableFuture containing a Result that wraps a txHash if the operation is successful. + * + * @param checkInterval the interval to check if the transaction is included in the block. It must not be null. + * @param logConsumer a consumer that processes log messages. It must not be null. + * @return a CompletableFuture containing a Result that wraps txHash if the operation is successful. + */ + public CompletableFuture> completeAndWaitAsync(@NonNull Duration checkInterval, + @NonNull Consumer logConsumer) { + return CompletableFuture.supplyAsync(() -> completeAndWait(Duration.ofSeconds(300), checkInterval, logConsumer)); + } + + /** + * Submit the transaction and return a CompletableFuture containing a Result that wraps a txHash if the operation is successful. + * + * @param checkInterval the interval to check if the transaction is included in the block. It must not be null. + * @param logConsumer a consumer that processes log messages. It must not be null. + * @param executor the executor to use for asynchronous execution. It must not be null. + * @return a CompletableFuture containing a Result that wraps txHash if the operation is successful. + */ + public CompletableFuture> completeAndWaitAsync(@NonNull Duration checkInterval, + @NonNull Consumer logConsumer, @NonNull Executor executor) { + + return CompletableFuture.supplyAsync(() -> { + Result result = complete(); + if (!result.isSuccessful()) + return result; + + logConsumer.accept(showStatus(Constant.STATUS_SUBMITTED, result.getValue())); + String txHash = result.getValue(); + try { + if (result.isSuccessful()) { //Wait for transaction to be included in the block + while (true) { + Optional utxoOptional = utxoSupplier.getTxOutput(txHash, 0); + if (utxoOptional.isPresent()) { + logConsumer.accept(showStatus(Constant.STATUS_CONFIRMED, txHash)); + return result; + } + + Thread.sleep(checkInterval.toMillis()); + } + } + } catch (Exception e) { + log.error("Error while waiting for transaction to be included in the block. TxHash : " + txHash, e); + logConsumer.accept("Error while waiting for transaction to be included in the block. TxHash : " + txHash); + } + + logConsumer.accept(showStatus(Constant.STATUS_TIMEOUT, txHash)); + return result; + }); + } + + private String showStatus(String status, String txHash) { return String.format("[%s] Tx: %s", status, txHash); }