From 59bbb3cce645c1ed858c8f1c79bc7f40b50347e3 Mon Sep 17 00:00:00 2001 From: James Piechota Date: Thu, 9 Jan 2025 19:19:53 +0100 Subject: [PATCH] Feature/ecdsa (#686) * ECDSA support * RFC 6979 signature + ecrecover --------- Co-authored-by: Lev Berman Co-authored-by: Saam Tehrani Co-authored-by: vird --- .gitmodules | 3 + apps/arweave/c_src/Makefile | 19 +- apps/arweave/c_src/secp256k1/secp256k1_nif.c | 209 +++++++++++++++++++ apps/arweave/include/ar.hrl | 7 + apps/arweave/lib/secp256k1 | 1 + apps/arweave/src/ar.erl | 35 +++- apps/arweave/src/ar_block.erl | 31 ++- apps/arweave/src/ar_node.erl | 4 +- apps/arweave/src/ar_node_utils.erl | 27 ++- apps/arweave/src/ar_node_worker.erl | 72 +++++-- apps/arweave/src/ar_serialize.erl | 173 ++++++++++----- apps/arweave/src/ar_tx.erl | 90 +++++++- apps/arweave/src/ar_wallet.erl | 78 +++---- apps/arweave/src/ar_wallets.erl | 2 +- apps/arweave/src/secp256k1_nif.erl | 30 +++ apps/arweave/test/ar_ecdsa_tests.erl | 40 ++++ bin/create-ecdsa-wallet | 11 + build_secp256k1.sh | 18 ++ rebar.config | 4 + 19 files changed, 694 insertions(+), 160 deletions(-) create mode 100755 apps/arweave/c_src/secp256k1/secp256k1_nif.c create mode 160000 apps/arweave/lib/secp256k1 create mode 100755 apps/arweave/src/secp256k1_nif.erl create mode 100755 apps/arweave/test/ar_ecdsa_tests.erl create mode 100755 bin/create-ecdsa-wallet create mode 100755 build_secp256k1.sh diff --git a/.gitmodules b/.gitmodules index f6bf3c1d9..0641c9f92 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/RandomX"] path = apps/arweave/lib/RandomX url = https://github.com/ArweaveTeam/RandomX.git +[submodule "apps/arweave/lib/secp256k1"] + path = apps/arweave/lib/secp256k1 + url = https://github.com/bitcoin-core/secp256k1 diff --git a/apps/arweave/c_src/Makefile b/apps/arweave/c_src/Makefile index 394dcb711..2e74b096c 100644 --- a/apps/arweave/c_src/Makefile +++ b/apps/arweave/c_src/Makefile @@ -66,7 +66,7 @@ C_SRC_DIR = $(CURDIR) CFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) -I /usr/local/include -I ../lib/RandomX/src -I $(C_SRC_DIR) CXXFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) -I ../lib/RandomX/src -std=c++11 -LDLIBS += -L $(ERL_INTERFACE_LIB_DIR) -L /usr/local/lib -lei -lssl -lcrypto +LDLIBS += -L $(ERL_INTERFACE_LIB_DIR) -L /usr/local/lib -lei -lssl -lcrypto RX512_OUTPUT ?= $(CURDIR)/../priv/rx512_arweave.so @@ -116,6 +116,19 @@ $(VDF_OUTPUT): $(VDF_OBJECTS) @mkdir -p $(BASEDIR)/priv/ $(link_verbose) $(CXX) $(VDF_OBJECTS) $(LDFLAGS) $(LDLIBS) -shared -o $(VDF_OUTPUT) +SECP256K1_SOURCES = $(wildcard $(C_SRC_DIR)/*.c $(C_SRC_DIR)/secp256k1/*.c) +SECP256K1_OBJECTS = $(addsuffix .o, $(basename $(SECP256K1_SOURCES))) +SECP256K1_CFLAGS = -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) -I /usr/local/include -I $(CURDIR)/../lib/secp256k1/src -I $(CURDIR)/../lib/secp256k1/include -I $(C_SRC_DIR) +SECP256K1_LDLIBS = -L $(ERL_INTERFACE_LIB_DIR) -L /usr/local/lib -lcrypto +SECP256K1_OUTPUT ?= $(CURDIR)/../priv/secp256k1_arweave.so + +$(SECP256K1_OUTPUT): $(SECP256K1_OBJECTS) + @mkdir -p $(BASEDIR)/priv/ + $(link_verbose) $(CXX) $(SECP256K1_OBJECTS) $(SECP256K1_LDLIBS) ../lib/secp256k1/build/lib/libsecp256k1.a -shared -o $(SECP256K1_OUTPUT) + +%secp256k1_nif.o: %secp256k1_nif.c + $(c_verbose) $(CC) $(SECP256K1_CFLAGS) -c $(OUTPUT_OPTION) $< + %.o: %.c $(COMPILE_C) $(OUTPUT_OPTION) $< @@ -128,10 +141,10 @@ $(VDF_OUTPUT): $(VDF_OBJECTS) %.o: %.cpp $(COMPILE_CPP) $(OUTPUT_OPTION) $< -all: $(RX512_OUTPUT) $(RX4096_OUTPUT) $(RXSQUARED_OUTPUT) $(VDF_OUTPUT) +all: $(RX512_OUTPUT) $(RX4096_OUTPUT) $(RXSQUARED_OUTPUT) $(VDF_OUTPUT) $(SECP256K1_OUTPUT) clean: - @rm -f $(RX512_OUTPUT) $(RX4096_OUTPUT) $(RXSQUARED_OUTPUT) $(VDF_OUTPUT) $(RX512_OBJECTS) $(RX4096_OBJECTS) $(RXSQUARED_OBJECTS) $(VDF_OBJECTS) + @rm -f $(RX512_OUTPUT) $(RX4096_OUTPUT) $(RXSQUARED_OUTPUT) $(VDF_OUTPUT) $(RX512_OBJECTS) $(RX4096_OBJECTS) $(RXSQUARED_OBJECTS) $(VDF_OBJECTS) $(SECP256K1_OUTPUT) $(SECP256K1_OBJECTS) diff --git a/apps/arweave/c_src/secp256k1/secp256k1_nif.c b/apps/arweave/c_src/secp256k1/secp256k1_nif.c new file mode 100755 index 000000000..be14a3c6a --- /dev/null +++ b/apps/arweave/c_src/secp256k1/secp256k1_nif.c @@ -0,0 +1,209 @@ +#include + +#include +#include +#include + +#include + +#define SECP256K1_PUBKEY_UNCOMPRESSED_SIZE 65 +#define SECP256K1_PUBKEY_COMPRESSED_SIZE 33 +#define SECP256K1_SIGNATURE_COMPACT_SIZE 64 +#define SECP256K1_SIGNATURE_RECOVERABLE_SIZE 65 +#define SECP256K1_PRIVKEY_SIZE 32 +#define SECP256K1_CONTEXT_SEED_SIZE 32 +#define SECP256K1_DIGEST_SIZE 32 + +static int secp256k1_load(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info) { + return 0; +} + +#if defined(_MSC_VER) +// For SecureZeroMemory +#include +#endif +/* Cleanses memory to prevent leaking sensitive info. Won't be optimized out. */ +static void secure_erase(void *ptr, size_t len) { +#if defined(_MSC_VER) + /* SecureZeroMemory is guaranteed not to be optimized out by MSVC. */ + SecureZeroMemory(ptr, len); +#elif defined(__GNUC__) + /* We use a memory barrier that scares the compiler away from optimizing out the memset. + * + * Quoting Adam Langley in commit ad1907fe73334d6c696c8539646c21b11178f20f + * in BoringSSL (ISC License): + * As best as we can tell, this is sufficient to break any optimisations that + * might try to eliminate "superfluous" memsets. + * This method used in memzero_explicit() the Linux kernel, too. Its advantage is that it is + * pretty efficient, because the compiler can still implement the memset() efficiently, + * just not remove it entirely. See "Dead Store Elimination (Still) Considered Harmful" by + * Yang et al. (USENIX Security 2017) for more background. + */ + memset(ptr, 0, len); + __asm__ __volatile__("" : : "r"(ptr) : "memory"); +#else + void *(*volatile const volatile_memset)(void *, int, size_t) = memset; + volatile_memset(ptr, 0, len); +#endif +} + +static ERL_NIF_TERM sign_recoverable(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { + if (argc != 2) { + return enif_make_badarg(env); + } + ErlNifBinary Digest, PrivateBytes; + if (!enif_inspect_binary(env, argv[0], &Digest)) { + return enif_make_badarg(env); + } + if (Digest.size != SECP256K1_DIGEST_SIZE) { + return enif_make_badarg(env); + } + + if (!enif_inspect_binary(env, argv[1], &PrivateBytes)) { + return enif_make_badarg(env); + } + if (PrivateBytes.size != SECP256K1_PRIVKEY_SIZE) { + return enif_make_badarg(env); + } + + char *error = NULL; + unsigned char seed[SECP256K1_CONTEXT_SEED_SIZE]; + unsigned char digest[SECP256K1_DIGEST_SIZE]; + unsigned char privbytes[SECP256K1_PRIVKEY_SIZE]; + unsigned char signature_compact[SECP256K1_SIGNATURE_COMPACT_SIZE]; + unsigned char signature_recoverable[SECP256K1_SIGNATURE_RECOVERABLE_SIZE]; + int recid; + secp256k1_ecdsa_recoverable_signature s; + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + memcpy(digest, Digest.data, SECP256K1_DIGEST_SIZE); + memcpy(privbytes, PrivateBytes.data, SECP256K1_PRIVKEY_SIZE); + + if (!secp256k1_ec_seckey_verify(ctx, privbytes)) { + error = "secp256k1 key is invalid."; + goto cleanup; + } + + if (!RAND_priv_bytes(seed, sizeof(seed))) { + error = "Failed to generate random seed for context."; + goto cleanup; + } + + if (!secp256k1_context_randomize(ctx, seed)) { + error = "Failed to randomize context."; + goto cleanup; + } + + if(!secp256k1_ecdsa_sign_recoverable(ctx, &s, digest, privbytes, NULL, NULL)) { + error = "Failed to create signature."; + goto cleanup; + } + + if(!secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, signature_compact, &recid, &s)) { + error = "Failed to serialize signature."; + goto cleanup; + } + memcpy(signature_recoverable, signature_compact, SECP256K1_SIGNATURE_COMPACT_SIZE); + signature_recoverable[64] = (unsigned char)(recid); + + ERL_NIF_TERM signature_term = make_output_binary(env, signature_recoverable, SECP256K1_SIGNATURE_RECOVERABLE_SIZE); + +cleanup: + secp256k1_context_destroy(ctx); + secure_erase(seed, sizeof(seed)); + secure_erase(privbytes, sizeof(privbytes)); + memset(signature_compact, 0, SECP256K1_SIGNATURE_COMPACT_SIZE); + memset(signature_recoverable, 0, SECP256K1_SIGNATURE_RECOVERABLE_SIZE); + + if (error) { + return error_tuple(env, error); + } + return ok_tuple(env, signature_term); +} + +static ERL_NIF_TERM recover_pk_and_verify(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { + if (argc != 2) { + return enif_make_badarg(env); + } + ErlNifBinary Digest, Signature; + if (!enif_inspect_binary(env, argv[0], &Digest)) { + return enif_make_badarg(env); + } + if (Digest.size != SECP256K1_DIGEST_SIZE) { + return enif_make_badarg(env); + } + + if (!enif_inspect_binary(env, argv[1], &Signature)) { + return enif_make_badarg(env); + } + if (Signature.size != SECP256K1_SIGNATURE_RECOVERABLE_SIZE) { + return enif_make_badarg(env); + } + + char *error = NULL; + unsigned char digest[SECP256K1_DIGEST_SIZE]; + unsigned char signature_recoverable[SECP256K1_SIGNATURE_RECOVERABLE_SIZE]; + unsigned char signature_compact[SECP256K1_SIGNATURE_COMPACT_SIZE]; + unsigned char pubbytes[SECP256K1_PUBKEY_COMPRESSED_SIZE]; + int recid; + secp256k1_ecdsa_recoverable_signature rs; + secp256k1_ecdsa_signature s; + secp256k1_pubkey pubkey; + + memcpy(digest, Digest.data, SECP256K1_DIGEST_SIZE); + memcpy(signature_recoverable, Signature.data, SECP256K1_SIGNATURE_RECOVERABLE_SIZE); + + memcpy(signature_compact, signature_recoverable, SECP256K1_SIGNATURE_COMPACT_SIZE); + recid = (int)signature_recoverable[64]; + + if (recid < 0 || recid > 3) { + error = "Invalid signature recid. recid >= 0 && recid <= 3."; + goto cleanup; + } + + if (!secp256k1_ecdsa_recoverable_signature_parse_compact(secp256k1_context_static, &rs, signature_compact, recid)) { + error = "Failed to deserialize/parse recoverable signature."; + goto cleanup; + } + + if (!secp256k1_ecdsa_recover(secp256k1_context_static, &pubkey, &rs, digest)) { + error = "Failed to recover public key."; + goto cleanup; + } + size_t l = SECP256K1_PUBKEY_COMPRESSED_SIZE; + if (!secp256k1_ec_pubkey_serialize(secp256k1_context_static, pubbytes, &l, &pubkey, SECP256K1_EC_COMPRESSED)) { + error = "Failed to serialize the recovered public key."; + goto cleanup; + } + + if (!secp256k1_ecdsa_recoverable_signature_convert(secp256k1_context_static, &s, &rs)) { + error = "Failed to convert recoverable signature to compact signature."; + goto cleanup; + } + + // NOTE. https://github.com/bitcoin-core/secp256k1/blob/f79f46c70386c693ff4e7aef0b9e7923ba284e56/src/secp256k1.c#L461 + // Verify performs check for low-s + int is_valid = secp256k1_ecdsa_verify(secp256k1_context_static, &s, digest, &pubkey); + ERL_NIF_TERM pubkey_term = make_output_binary(env, pubbytes, SECP256K1_PUBKEY_COMPRESSED_SIZE); + +cleanup: + memset(digest, 0, SECP256K1_DIGEST_SIZE); + memset(pubbytes, 0, SECP256K1_PUBKEY_COMPRESSED_SIZE); + memset(signature_compact, 0, SECP256K1_SIGNATURE_COMPACT_SIZE); + memset(signature_recoverable, 0, SECP256K1_SIGNATURE_RECOVERABLE_SIZE); + + if (error) { + return error_tuple(env, error); + } + if (is_valid) { + return ok_tuple2(env, enif_make_atom(env, "true"), pubkey_term); + } + return ok_tuple2(env, enif_make_atom(env, "false"), pubkey_term); +} + +static ErlNifFunc nif_funcs[] = { + {"sign_recoverable", 2, sign_recoverable}, + {"recover_pk_and_verify", 2, recover_pk_and_verify} +}; + +ERL_NIF_INIT(secp256k1_nif, nif_funcs, secp256k1_load, NULL, NULL, NULL) \ No newline at end of file diff --git a/apps/arweave/include/ar.hrl b/apps/arweave/include/ar.hrl index 030292958..3117f3861 100644 --- a/apps/arweave/include/ar.hrl +++ b/apps/arweave/include/ar.hrl @@ -66,6 +66,13 @@ %% The default key type used by transactions that do not specify a signature type. -define(DEFAULT_KEY_TYPE, {?RSA_SIGN_ALG, 65537}). +-define(RSA_KEY_TYPE, {?RSA_SIGN_ALG, 65537}). +-define(ECDSA_KEY_TYPE, {?ECDSA_SIGN_ALG, secp256k1}). + +-define(RSA_BLOCK_SIG_SIZE, 512). +-define(ECDSA_PUB_KEY_SIZE, 33). +-define(ECDSA_SIG_SIZE, 65). + %% The difficulty a new weave is started with. -define(DEFAULT_DIFF, 6). diff --git a/apps/arweave/lib/secp256k1 b/apps/arweave/lib/secp256k1 new file mode 160000 index 000000000..f79f46c70 --- /dev/null +++ b/apps/arweave/lib/secp256k1 @@ -0,0 +1 @@ +Subproject commit f79f46c70386c693ff4e7aef0b9e7923ba284e56 diff --git a/apps/arweave/src/ar.erl b/apps/arweave/src/ar.erl index 8a523f08a..1173d68b0 100644 --- a/apps/arweave/src/ar.erl +++ b/apps/arweave/src/ar.erl @@ -6,6 +6,7 @@ -behaviour(application). -export([main/0, main/1, create_wallet/0, create_wallet/1, + create_ecdsa_wallet/0, create_ecdsa_wallet/1, benchmark_packing/1, benchmark_packing/0, benchmark_2_9/0, benchmark_2_9/1, benchmark_vdf/0, benchmark_hash/1, benchmark_hash/0, start/0, @@ -13,9 +14,9 @@ tests/0, tests/1, tests/2, e2e/0, e2e/1, shell/0, stop_shell/0, docs/0, shutdown/1, console/1, console/2]). --include_lib("arweave/include/ar.hrl"). --include_lib("arweave/include/ar_consensus.hrl"). --include_lib("arweave/include/ar_config.hrl"). +-include("../include/ar.hrl"). +-include("../include/ar_consensus.hrl"). +-include("../include/ar_config.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -794,12 +795,25 @@ set_mining_address(#config{ mining_addr = Addr, cm_exit_peer = CmExitPeer, end. create_wallet([DataDir]) -> + create_wallet(DataDir, ?RSA_KEY_TYPE); +create_wallet(_) -> + create_wallet_fail(?RSA_KEY_TYPE). + +create_ecdsa_wallet() -> + create_wallet_fail(?ECDSA_KEY_TYPE). + +create_ecdsa_wallet([DataDir]) -> + create_wallet(DataDir, ?ECDSA_KEY_TYPE); +create_ecdsa_wallet(_) -> + create_wallet_fail(?ECDSA_KEY_TYPE). + +create_wallet(DataDir, KeyType) -> case filelib:is_dir(DataDir) of false -> - create_wallet_fail(); + create_wallet_fail(KeyType); true -> ok = application:set_env(arweave, config, #config{ data_dir = DataDir }), - case ar_wallet:new_keyfile({?RSA_SIGN_ALG, 65537}) of + case ar_wallet:new_keyfile(KeyType) of {error, Reason} -> ar:console("Failed to create a wallet, reason: ~p.~n~n", [io_lib:format("~p", [Reason])]), @@ -810,15 +824,16 @@ create_wallet([DataDir]) -> ar:console("Created a wallet with address ~s.~n", [ar_util:encode(Addr)]), erlang:halt() end - end; -create_wallet(_) -> - create_wallet_fail(). + end. create_wallet() -> - create_wallet_fail(). + create_wallet_fail(?RSA_KEY_TYPE). -create_wallet_fail() -> +create_wallet_fail(?RSA_KEY_TYPE) -> io:format("Usage: ./bin/create-wallet [data_dir]~n"), + erlang:halt(); +create_wallet_fail(?ECDSA_KEY_TYPE) -> + io:format("Usage: ./bin/create-ecdsa-wallet [data_dir]~n"), erlang:halt(). benchmark_packing() -> diff --git a/apps/arweave/src/ar_block.erl b/apps/arweave/src/ar_block.erl index 882ef08a8..71adb35b1 100644 --- a/apps/arweave/src/ar_block.erl +++ b/apps/arweave/src/ar_block.erl @@ -23,10 +23,11 @@ get_sub_chunk_index/2, get_chunk_padded_offset/1]). --include_lib("arweave/include/ar.hrl"). --include_lib("arweave/include/ar_consensus.hrl"). --include_lib("arweave/include/ar_block.hrl"). --include_lib("arweave/include/ar_vdf.hrl"). +-include("../include/ar.hrl"). +-include("../include/ar_consensus.hrl"). +-include("../include/ar_block.hrl"). +-include("../include/ar_vdf.hrl"). + -include_lib("eunit/include/eunit.hrl"). %%%=================================================================== @@ -403,7 +404,7 @@ generate_signed_hash(#block{ previous_block = PrevH, timestamp = TS, RewardHistoryHash:32/binary, (encode_int(DebtSupply, 8))/binary, KryderPlusRateMultiplier:24, KryderPlusRateMultiplierLatch:8, Denomination:24, (encode_int(RedenominationHeight, 8))/binary, - (ar_serialize:encode_double_signing_proof(DoubleSigningProof))/binary, + (ar_serialize:encode_double_signing_proof(DoubleSigningProof, Height))/binary, (encode_int(PrevCDiff, 16))/binary, RebaseThresholdBin/binary, DataPathBin/binary, TXPathBin/binary, DataPath2Bin/binary, TXPath2Bin/binary, ChunkHashBin/binary, Chunk2HashBin/binary, BlockTimeHistoryHashBin/binary, @@ -428,15 +429,31 @@ indep_hash(BDS, B) -> %% @doc Verify the block signature. verify_signature(BlockPreimage, PrevCDiff, - #block{ signature = Signature, reward_key = {?DEFAULT_KEY_TYPE, Pub} = RewardKey, + #block{ signature = Signature, reward_key = {?RSA_KEY_TYPE, Pub} = RewardKey, reward_addr = RewardAddr, previous_solution_hash = PrevSolutionH, cumulative_diff = CDiff }) - when byte_size(Signature) == 512, byte_size(Pub) == 512 -> + when byte_size(Signature) == ?RSA_BLOCK_SIG_SIZE, + byte_size(Pub) == ?RSA_BLOCK_SIG_SIZE -> SignaturePreimage = << (ar_serialize:encode_int(CDiff, 16))/binary, (ar_serialize:encode_int(PrevCDiff, 16))/binary, PrevSolutionH/binary, BlockPreimage/binary >>, ar_wallet:to_address(RewardKey) == RewardAddr andalso ar_wallet:verify(RewardKey, SignaturePreimage, Signature); +verify_signature(BlockPreimage, PrevCDiff, + #block{ signature = Signature, reward_key = {?ECDSA_KEY_TYPE, Pub} = RewardKey, + reward_addr = RewardAddr, previous_solution_hash = PrevSolutionH, + cumulative_diff = CDiff, height = Height }) + when byte_size(Signature) == ?ECDSA_SIG_SIZE, byte_size(Pub) == ?ECDSA_PUB_KEY_SIZE -> + SignaturePreimage = << (ar_serialize:encode_int(CDiff, 16))/binary, + (ar_serialize:encode_int(PrevCDiff, 16))/binary, PrevSolutionH/binary, + BlockPreimage/binary >>, + case Height >= ar_fork:height_2_9() of + true -> + ar_wallet:to_address(RewardKey) == RewardAddr andalso + ar_wallet:verify(RewardKey, SignaturePreimage, Signature); + false -> + false + end; verify_signature(_BlockPreimage, _PrevCDiff, _B) -> false. diff --git a/apps/arweave/src/ar_node.erl b/apps/arweave/src/ar_node.erl index 281c0cc71..d2ff5ff00 100644 --- a/apps/arweave/src/ar_node.erl +++ b/apps/arweave/src/ar_node.erl @@ -210,7 +210,7 @@ get_balance({SigType, PubKey}) -> get_balance(ar_wallet:to_address(PubKey, SigType)); get_balance(MaybeRSAPub) when byte_size(MaybeRSAPub) == 512 -> %% A legacy feature where we may search the public key instead of address. - ar_wallets:get_balance(ar_wallet:to_rsa_address(MaybeRSAPub)); + ar_wallets:get_balance(ar_wallet:hash_pub_key(MaybeRSAPub)); get_balance(Addr) -> ar_wallets:get_balance(Addr). @@ -220,7 +220,7 @@ get_last_tx({SigType, PubKey}) -> get_last_tx(ar_wallet:to_address(PubKey, SigType)); get_last_tx(MaybeRSAPub) when byte_size(MaybeRSAPub) == 512 -> %% A legacy feature where we may search the public key instead of address. - get_last_tx(ar_wallet:to_rsa_address(MaybeRSAPub)); + get_last_tx(ar_wallet:hash_pub_key(MaybeRSAPub)); get_last_tx(Addr) -> {ok, ar_wallets:get_last_tx(Addr)}. diff --git a/apps/arweave/src/ar_node_utils.erl b/apps/arweave/src/ar_node_utils.erl index e3566147a..f076c8fe7 100644 --- a/apps/arweave/src/ar_node_utils.erl +++ b/apps/arweave/src/ar_node_utils.erl @@ -6,10 +6,10 @@ block_passes_diff_check/1, block_passes_diff_check/2, passes_diff_check/4, update_account/6, is_account_banned/2]). --include_lib("arweave/include/ar.hrl"). --include_lib("arweave/include/ar_pricing.hrl"). --include_lib("arweave/include/ar_consensus.hrl"). --include_lib("arweave/include/ar_mining.hrl"). +-include("../include/ar.hrl"). +-include("../include/ar_pricing.hrl"). +-include("../include/ar_consensus.hrl"). +-include("../include/ar_mining.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -248,10 +248,23 @@ may_be_apply_double_signing_proof(B, PrevB, Accounts) -> may_be_apply_double_signing_proof2(B, PrevB, Accounts) end. +get_reward_key(Pub, Height) -> + case Height >= ar_fork:height_2_9() of + false -> + {?DEFAULT_KEY_TYPE, Pub}; + true -> + case byte_size(Pub) of + ?ECDSA_PUB_KEY_SIZE -> + {?ECDSA_KEY_TYPE, Pub}; + _ -> + {?RSA_KEY_TYPE, Pub} + end + end. + may_be_apply_double_signing_proof2(B, PrevB, Accounts) -> {Pub, _Signature1, _CDiff1, _PrevCDiff1, _Preimage1, _Signature2, _CDiff2, _PrevCDiff2, _Preimage2} = B#block.double_signing_proof, - Key = {?DEFAULT_KEY_TYPE, Pub}, + Key = get_reward_key(Pub, B#block.height), case B#block.reward_key == Key of true -> {error, invalid_double_signing_proof_same_address}; @@ -278,7 +291,7 @@ may_be_apply_double_signing_proof3(B, PrevB, Accounts) -> EncodedPrevCDiff1 = ar_serialize:encode_int(PrevCDiff1, 16), SignaturePreimage1 = << EncodedCDiff1/binary, EncodedPrevCDiff1/binary, Preimage1/binary >>, - Key = {?DEFAULT_KEY_TYPE, Pub}, + Key = get_reward_key(Pub, B#block.height), Addr = ar_wallet:to_address(Key), case ar_wallet:verify(Key, SignaturePreimage1, Signature1) of false -> @@ -320,7 +333,7 @@ update_accounts4(B, PrevB, Accounts, Args) -> update_accounts5(B, Accounts, Args); Proof -> Denomination = PrevB#block.denomination, - BannedAddr = ar_wallet:to_address({?DEFAULT_KEY_TYPE, element(1, Proof)}), + BannedAddr = ar_wallet:hash_pub_key(element(1, Proof)), Sum = ar_rewards:get_total_reward_for_address(BannedAddr, PrevB) - 1, {Dividend, Divisor} = ?DOUBLE_SIGNING_PROVER_REWARD_SHARE, LockedRewards = ar_rewards:get_locked_rewards(PrevB), diff --git a/apps/arweave/src/ar_node_worker.erl b/apps/arweave/src/ar_node_worker.erl index e8d9f314a..9d67a01a1 100644 --- a/apps/arweave/src/ar_node_worker.erl +++ b/apps/arweave/src/ar_node_worker.erl @@ -14,13 +14,14 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). -export([set_reward_addr/1]). --include_lib("arweave/include/ar.hrl"). --include_lib("arweave/include/ar_consensus.hrl"). --include_lib("arweave/include/ar_config.hrl"). --include_lib("arweave/include/ar_pricing.hrl"). --include_lib("arweave/include/ar_data_sync.hrl"). --include_lib("arweave/include/ar_vdf.hrl"). --include_lib("arweave/include/ar_mining.hrl"). +-include("../include/ar.hrl"). +-include("../include/ar_consensus.hrl"). +-include("../include/ar_config.hrl"). +-include("../include/ar_pricing.hrl"). +-include("../include/ar_data_sync.hrl"). +-include("../include/ar_vdf.hrl"). +-include("../include/ar_mining.hrl"). + -include_lib("eunit/include/eunit.hrl"). -ifdef(AR_TEST). @@ -493,7 +494,7 @@ handle_info({tx_ready_for_mining, TX}, State) -> handle_info({event, block, {double_signing, Proof}}, State) -> Map = maps:get(double_signing_proofs, State, #{}), Key = element(1, Proof), - Addr = ar_wallet:to_address({?DEFAULT_KEY_TYPE, Key}), + Addr = ar_wallet:hash_pub_key(Key), case is_map_key(Addr, Map) of true -> {noreply, State}; @@ -1010,21 +1011,43 @@ may_be_get_double_signing_proof(PrevB, State) -> LockedRewards = ar_rewards:get_locked_rewards(PrevB), Proofs = maps:get(double_signing_proofs, State, #{}), RootHash = PrevB#block.wallet_list, - may_be_get_double_signing_proof2(maps:iterator(Proofs), RootHash, LockedRewards). + Height = PrevB#block.height + 1, + may_be_get_double_signing_proof2(maps:iterator(Proofs), RootHash, LockedRewards, Height). -may_be_get_double_signing_proof2(Iterator, RootHash, LockedRewards) -> +may_be_get_double_signing_proof2(Iterator, RootHash, LockedRewards, Height) -> case maps:next(Iterator) of none -> undefined; {Addr, {_Timestamp, Proof2}, Iterator2} -> - case ar_rewards:has_locked_reward(Addr, LockedRewards) of + {Key, Sig1, _CDiff1, _PrevCDiff1, _Preimage1, + Sig2, _CDiff2, _PrevCDiff2, _Preimage2} = Proof2, + CheckKeyType = + case {byte_size(Key) == ?ECDSA_PUB_KEY_SIZE, Height >= ar_fork:height_2_9()} of + {true, false} -> + false; + {true, true} -> + byte_size(Sig1) == ?ECDSA_SIG_SIZE + andalso byte_size(Sig2) == ?ECDSA_SIG_SIZE; + _ -> + true + end, + HasLockedReward = + case CheckKeyType of + false -> + false; + true -> + ar_rewards:has_locked_reward(Addr, LockedRewards) + end, + case HasLockedReward of false -> - may_be_get_double_signing_proof2(Iterator2, RootHash, LockedRewards); + may_be_get_double_signing_proof2(Iterator2, + RootHash, LockedRewards, Height); true -> Accounts = ar_wallets:get(RootHash, [Addr]), case ar_node_utils:is_account_banned(Addr, Accounts) of true -> - may_be_get_double_signing_proof2(Iterator2, RootHash, LockedRewards); + may_be_get_double_signing_proof2(Iterator2, + RootHash, LockedRewards, Height); false -> Proof2 end @@ -1075,7 +1098,7 @@ pack_block_with_transactions(B, PrevB) -> undefined -> Addresses2; Proof -> - [ar_wallet:to_address({?DEFAULT_KEY_TYPE, element(1, Proof)}) | Addresses2] + [ar_wallet:hash_pub_key(element(1, Proof)) | Addresses2] end, Accounts = ar_wallets:get(PrevB#block.wallet_list, Addresses3), [{block_txs_pairs, BlockTXPairs}] = ets:lookup(node_state, block_txs_pairs), @@ -2124,6 +2147,7 @@ handle_found_solution(Args, PrevB, State) -> SignaturePreimage = << (ar_serialize:encode_int(CDiff, 16))/binary, (ar_serialize:encode_int(PrevCDiff, 16))/binary, (PrevB#block.hash)/binary, SignedH/binary >>, + assert_key_type(RewardKey, Height), Signature = ar_wallet:sign(element(1, RewardKey), SignaturePreimage), H = ar_block:indep_hash2(SignedH, Signature), B = UnsignedB2#block{ indep_hash = H, signature = Signature }, @@ -2154,6 +2178,26 @@ handle_found_solution(Args, PrevB, State) -> {noreply, State} end. +assert_key_type(RewardKey, Height) -> + case Height >= ar_fork:height_2_9() of + false -> + case RewardKey of + {{?RSA_KEY_TYPE, _, _}, {?RSA_KEY_TYPE, _}} -> + ok; + _ -> + exit(invalid_reward_key) + end; + true -> + case RewardKey of + {{?RSA_KEY_TYPE, _, _}, {?RSA_KEY_TYPE, _}} -> + ok; + {{?ECDSA_KEY_TYPE, _, _}, {?ECDSA_KEY_TYPE, _}} -> + ok; + _ -> + exit(invalid_reward_key) + end + end. + update_solution_cache(H, Args, State) -> %% Maintain a cache of mining solutions for potential reuse in rebasing. %% diff --git a/apps/arweave/src/ar_serialize.erl b/apps/arweave/src/ar_serialize.erl index 6c7ae59ac..cfbe27419 100644 --- a/apps/arweave/src/ar_serialize.erl +++ b/apps/arweave/src/ar_serialize.erl @@ -12,7 +12,7 @@ poa_no_chunk_map_to_binary/1, binary_to_no_chunk_map/1, poa_map_to_json_map/1, poa_no_chunk_map_to_json_map/1, json_map_to_poa_map/1, - block_index_to_binary/1, binary_to_block_index/1, encode_double_signing_proof/1, + block_index_to_binary/1, binary_to_block_index/1, encode_double_signing_proof/2, json_struct_to_poa/1, poa_to_json_struct/1, tx_to_json_struct/1, json_struct_to_tx/1, json_struct_to_v1_tx/1, etf_to_wallet_chunk_response/1, wallet_list_to_json_struct/3, @@ -33,11 +33,11 @@ partial_solution_response_to_json_struct/1, pool_cm_jobs_to_json_struct/1, json_map_to_pool_cm_jobs/1]). --include_lib("arweave/include/ar.hrl"). --include_lib("arweave/include/ar_consensus.hrl"). --include_lib("arweave/include/ar_vdf.hrl"). --include_lib("arweave/include/ar_mining.hrl"). --include_lib("arweave/include/ar_pool.hrl"). +-include("../include/ar.hrl"). +-include("../include/ar_consensus.hrl"). +-include("../include/ar_vdf.hrl"). +-include("../include/ar_mining.hrl"). +-include("../include/ar_pool.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -630,15 +630,27 @@ binary_to_nonce_limiter_update_response( _SessionFoundBin, _StepNumberSize, _StepNumber, _Postpone, _Format) -> {error, invalid1}. -encode_double_signing_proof(undefined) -> +encode_double_signing_proof(undefined, _Height) -> << 0:8 >>; -encode_double_signing_proof(Proof) -> - {Key, Sig1, CDiff1, PrevCDiff1, Preimage1, Sig2, CDiff2, PrevCDiff2, Preimage2} = Proof, - << 1:8, Key:512/binary, Sig1:512/binary, - (ar_serialize:encode_int(CDiff1, 16))/binary, - (ar_serialize:encode_int(PrevCDiff1, 16))/binary, Preimage1:64/binary, - Sig2:512/binary, (ar_serialize:encode_int(CDiff2, 16))/binary, - (ar_serialize:encode_int(PrevCDiff2, 16))/binary, Preimage2:64/binary >>. +encode_double_signing_proof(Proof, Height) -> + {Key, Sig1, CDiff1, PrevCDiff1, Preimage1, + Sig2, CDiff2, PrevCDiff2, Preimage2} = Proof, + case Height >= ar_fork:height_2_9() of + false -> + << 1:8, Key:512/binary, Sig1:512/binary, + (ar_serialize:encode_int(CDiff1, 16))/binary, + (ar_serialize:encode_int(PrevCDiff1, 16))/binary, Preimage1:64/binary, + Sig2:512/binary, (ar_serialize:encode_int(CDiff2, 16))/binary, + (ar_serialize:encode_int(PrevCDiff2, 16))/binary, Preimage2:64/binary >>; + true -> + << 1:8, (ar_serialize:encode_bin(Key, 16))/binary, + (ar_serialize:encode_bin(Sig1, 16))/binary, + (ar_serialize:encode_int(CDiff1, 16))/binary, + (ar_serialize:encode_int(PrevCDiff1, 16))/binary, Preimage1:64/binary, + (ar_serialize:encode_bin(Sig2, 16))/binary, + (ar_serialize:encode_int(CDiff2, 16))/binary, + (ar_serialize:encode_int(PrevCDiff2, 16))/binary, Preimage2:64/binary >> + end. %%%=================================================================== %%% Private functions. @@ -676,7 +688,7 @@ encode_post_2_6_fields(#block{ height = Height, hash_preimage = HashPreimage, KryderPlusRateMultiplier:24, KryderPlusRateMultiplierLatch:8, Denomination:24, (encode_int(RedenominationHeight, 8))/binary, (encode_int(PrevCDiff, 16))/binary, - (encode_double_signing_proof(DoubleSigningProof))/binary, + (encode_double_signing_proof(DoubleSigningProof, Height))/binary, (encode_post_2_7_fields(B))/binary >> end. @@ -771,13 +783,19 @@ encode_tx(#tx{ format = Format, id = TXID, last_tx = LastTX, owner = Owner, tags = Tags, target = Target, quantity = Quantity, data = Data, data_size = DataSize, data_root = DataRoot, signature = Signature, reward = Reward, signature_type = SignatureType } = TX) -> + Owner2 = + case SignatureType of + ?ECDSA_KEY_TYPE -> + <<>>; + _ -> + Owner + end, << Format:8, TXID:32/binary, - (encode_bin(LastTX, 8))/binary, (encode_bin(Owner, 16))/binary, + (encode_bin(LastTX, 8))/binary, (encode_bin(Owner2, 16))/binary, (encode_bin(Target, 8))/binary, (encode_int(Quantity, 8))/binary, (encode_int(DataSize, 16))/binary, (encode_bin(DataRoot, 8))/binary, (encode_bin(Signature, 16))/binary, (encode_int(Reward, 8))/binary, (encode_bin(Data, 24))/binary, (encode_tx_tags(Tags))/binary, - (encode_signature_type(SignatureType))/binary, (may_be_encode_tx_denomination(TX))/binary >>. encode_tx_tags(Tags) -> @@ -791,13 +809,6 @@ encode_tx_tags([{Name, Value} | Tags], Encoded, N) -> Tag = << TagNameSize:16, TagValueSize:16, Name/binary, Value/binary >>, encode_tx_tags(Tags, [Tag | Encoded], N + 1). -encode_signature_type(?DEFAULT_KEY_TYPE) -> - <<>>; -encode_signature_type({?ECDSA_SIGN_ALG, secp256k1}) -> - << 1:8 >>; -encode_signature_type({?EDDSA_SIGN_ALG, ed25519}) -> - << 2:8 >>. - may_be_encode_tx_denomination(#tx{ denomination = 0 }) -> <<>>; may_be_encode_tx_denomination(#tx{ denomination = Denomination }) -> @@ -859,11 +870,18 @@ parse_block_post_2_6_fields(B, << HashPreimageSize:8, HashPreimage:HashPreimageS last_step_checkpoints = parse_checkpoints(LastCheckpoints, Height), steps = parse_checkpoints(Steps, Height) }, RecallByte2_2 = case RecallByte2Size of 0 -> undefined; _ -> RecallByte2 end, + SigType = + case {RewardKeySize, Height >= ar_fork:height_2_9()} of + {?ECDSA_PUB_KEY_SIZE, true} -> + ?ECDSA_KEY_TYPE; + _ -> + ?RSA_KEY_TYPE + end, B2 = B#block{ hash_preimage = HashPreimage, recall_byte = RecallByte_2, reward = Reward, nonce = Nonce, recall_byte2 = RecallByte2_2, previous_solution_hash = PreviousSolutionHash, signature = Sig, partition_number = PartitionNumber, - reward_key = {{?RSA_SIGN_ALG, 65537}, RewardKey}, + reward_key = {SigType, RewardKey}, nonce_limiter_info = NonceLimiterInfo, poa2 = #poa{ chunk = Chunk, data_path = DataPath, tx_path = TXPath }, price_per_gib_minute = PricePerGiBMinute, @@ -920,17 +938,35 @@ parse_block_transactions(_N, _Rest, _TXs) -> parse_double_signing_proof(<< 0:8, Rest/binary >>, B) -> parse_post_2_7_fields(Rest, B); -parse_double_signing_proof(<< 1:8, Key:512/binary, Sig1:512/binary, - CDiff1Size:16, CDiff1:(CDiff1Size * 8), - PrevCDiff1Size:16, PrevCDiff1:(PrevCDiff1Size * 8), - Preimage1:64/binary, Sig2:512/binary, CDiff2Size:16, CDiff2:(CDiff2Size * 8), - PrevCDiff2Size:16, PrevCDiff2:(PrevCDiff2Size * 8), - Preimage2:64/binary, Rest/binary >>, B) -> - B2 = B#block{ double_signing_proof = {Key, Sig1, CDiff1, PrevCDiff1, Preimage1, - Sig2, CDiff2, PrevCDiff2, Preimage2} }, - parse_post_2_7_fields(Rest, B2); -parse_double_signing_proof(_Bin, _B) -> - {error, invalid_double_signing_proof_input}. +parse_double_signing_proof(Bin, #block{ height = Height } = B) -> + case {Bin, Height >= ar_fork:height_2_9()} of + {<< 1:8, Key:512/binary, Sig1:512/binary, + CDiff1Size:16, CDiff1:(CDiff1Size * 8), + PrevCDiff1Size:16, PrevCDiff1:(PrevCDiff1Size * 8), + Preimage1:64/binary, Sig2:512/binary, + CDiff2Size:16, CDiff2:(CDiff2Size * 8), + PrevCDiff2Size:16, PrevCDiff2:(PrevCDiff2Size * 8), + Preimage2:64/binary, Rest/binary >>, false} -> + Proof = {Key, Sig1, CDiff1, PrevCDiff1, Preimage1, + Sig2, CDiff2, PrevCDiff2, Preimage2}, + B2 = B#block{ double_signing_proof = Proof }, + parse_post_2_7_fields(Rest, B2); + {_Bin, false} -> + {error, invalid_double_signing_proof_input}; + {<< 1:8, KeySize:16, Key:KeySize/binary, Sig1Size:16, Sig1:Sig1Size/binary, + CDiff1Size:16, CDiff1:(CDiff1Size * 8), + PrevCDiff1Size:16, PrevCDiff1:(PrevCDiff1Size * 8), + Preimage1:64/binary, Sig2Size:16, Sig2:Sig2Size/binary, + CDiff2Size:16, CDiff2:(CDiff2Size * 8), + PrevCDiff2Size:16, PrevCDiff2:(PrevCDiff2Size * 8), + Preimage2:64/binary, Rest/binary >>, true} -> + Proof = {Key, Sig1, CDiff1, PrevCDiff1, Preimage1, + Sig2, CDiff2, PrevCDiff2, Preimage2}, + B2 = B#block{ double_signing_proof = Proof }, + parse_post_2_7_fields(Rest, B2); + {_Bin, true} -> + {error, invalid_double_signing_proof_input2} +end. parse_post_2_7_fields(Rest, #block{ height = Height } = B) -> case {Rest, Height >= ar_fork:height_2_7()} of @@ -1012,13 +1048,23 @@ parse_tx(<< Format:8, TXID:32/binary, {error, Reason} -> {error, Reason}; {ok, Tags, Rest2} -> + SigType = set_sig_type_from_pub_key(Owner, Signature), case parse_tx_denomination(Rest2) of {ok, Denomination} -> DataSize2 = case Format of 1 -> byte_size(Data); _ -> DataSize end, - {ok, #tx{ format = Format, id = TXID, last_tx = LastTX, owner = Owner, + TX = #tx{ format = Format, id = TXID, last_tx = LastTX, owner = Owner, target = Target, quantity = Quantity, data_size = DataSize2, data_root = DataRoot, signature = Signature, reward = Reward, - data = Data, tags = Tags, denomination = Denomination }}; + data = Data, tags = Tags, denomination = Denomination, + signature_type = SigType }, + case SigType of + {?ECDSA_SIGN_ALG, secp256k1} -> + DataSegment = ar_tx:generate_signature_data_segment(TX), + Owner2 = ar_wallet:recover_key(DataSegment, Signature, SigType), + {ok, TX#tx{ owner = Owner2 }}; + {?RSA_SIGN_ALG, 65537} -> + {ok, TX} + end; {error, Reason} -> {error, Reason} end @@ -1047,15 +1093,6 @@ parse_tx_denomination(<< Denomination:24 >>) when Denomination > 0 -> parse_tx_denomination(_Rest) -> {error, invalid_denomination}. -%parse_signature_type(<<>>) -> -% {ok, ?DEFAULT_KEY_TYPE}; -%parse_signature_type(<< 1:8 >>) -> -% {ok, {?ECDSA_SIGN_ALG, secp256k1}}; -%parse_signature_type(<< 2:8 >>) -> -% {ok, {?EDDSA_SIGN_ALG, ed25519}}; -%parse_signature_type(_Rest) -> -% {error, invalid_input}. - tx_to_binary(TX) -> Bin = encode_tx(TX), TXSize = byte_size(Bin), @@ -1390,10 +1427,18 @@ tx_to_json_struct( data = Data, reward = Reward, signature = Sig, + signature_type = SigType, data_size = DataSize, data_root = DataRoot, denomination = Denomination }) -> + Owner2 = + case SigType of + ?ECDSA_KEY_TYPE -> + <<>>; + _ -> + Owner + end, Fields = [ {format, case Format of @@ -1404,7 +1449,7 @@ tx_to_json_struct( end}, {id, ar_util:encode(ID)}, {last_tx, ar_util:encode(Last)}, - {owner, ar_util:encode(Owner)}, + {owner, ar_util:encode(Owner2)}, {tags, lists:map( fun({Name, Value}) -> @@ -1549,11 +1594,14 @@ json_struct_to_tx(TXStruct, ComputeDataSize) -> end, TXID = ar_util:decode(find_value(<<"id">>, TXStruct)), 32 = byte_size(TXID), - #tx{ + Owner = ar_util:decode(find_value(<<"owner">>, TXStruct)), + Sig = ar_util:decode(find_value(<<"signature">>, TXStruct)), + SigType = set_sig_type_from_pub_key(Owner, Sig), + TX = #tx{ format = Format, id = TXID, last_tx = ar_util:decode(find_value(<<"last_tx">>, TXStruct)), - owner = ar_util:decode(find_value(<<"owner">>, TXStruct)), + owner = Owner, tags = [{ar_util:decode(Name), ar_util:decode(Value)} %% Only the elements matching this pattern are included in the list. || {[{<<"name">>, Name}, {<<"value">>, Value}]} <- Tags], @@ -1562,7 +1610,8 @@ json_struct_to_tx(TXStruct, ComputeDataSize) -> quantity = binary_to_integer(find_value(<<"quantity">>, TXStruct)), data = Data, reward = binary_to_integer(find_value(<<"reward">>, TXStruct)), - signature = ar_util:decode(find_value(<<"signature">>, TXStruct)), + signature = Sig, + signature_type = SigType, data_size = parse_data_size(Format, TXStruct, Data, ComputeDataSize), data_root = case find_value(<<"data_root">>, TXStruct) of @@ -1570,7 +1619,27 @@ json_struct_to_tx(TXStruct, ComputeDataSize) -> DR -> ar_util:decode(DR) end, denomination = Denomination - }. + }, + case SigType of + ?ECDSA_KEY_TYPE -> + DataSegment = ar_tx:generate_signature_data_segment(TX), + Owner2 = ar_wallet:recover_key(DataSegment, Sig, SigType), + TX#tx{ owner = Owner2 }; + ?RSA_KEY_TYPE -> + TX + end. + +set_sig_type_from_pub_key(Owner, <<>>) -> + %% Transactions with the empty signatures are used in some old tests, + %% e.g., ar_http_iface_tests.erl. + ?RSA_KEY_TYPE; +set_sig_type_from_pub_key(Owner, _Sig) -> + case Owner of + <<>> -> + ?ECDSA_KEY_TYPE; + _ -> + ?RSA_KEY_TYPE + end. json_list_to_diff_pair(List) -> [PoA1DiffBin, DiffBin] = diff --git a/apps/arweave/src/ar_tx.erl b/apps/arweave/src/ar_tx.erl index 548830c0f..a0b15ddcd 100644 --- a/apps/arweave/src/ar_tx.erl +++ b/apps/arweave/src/ar_tx.erl @@ -2,13 +2,15 @@ -module(ar_tx). -export([new/0, new/1, new/2, new/3, new/4, sign/2, sign/3, sign_v1/2, sign_v1/3, verify/2, - verify/3, verify_tx_id/2, tags_to_list/1, get_tx_fee/1, get_tx_fee2/1, check_last_tx/2, + verify/3, verify_tx_id/2, generate_signature_data_segment/1, + tags_to_list/1, get_tx_fee/1, get_tx_fee2/1, check_last_tx/2, generate_chunk_tree/1, generate_chunk_tree/2, generate_chunk_id/1, chunk_binary/2, chunks_to_size_tagged_chunks/1, sized_chunks_to_sized_chunk_ids/1, get_addresses/1, get_weave_size_increase/2, utility/1]). --include_lib("arweave/include/ar.hrl"). --include_lib("arweave/include/ar_pricing.hrl"). +-include("../include/ar.hrl"). +-include("../include/ar_pricing.hrl"). + -include_lib("eunit/include/eunit.hrl"). %% Prioritize format=1 transactions with data size bigger than this @@ -111,6 +113,17 @@ verify_tx_id(ExpectedID, #tx{ format = 1, id = ID } = TX) -> verify_tx_id(ExpectedID, #tx{ format = 2, id = ID } = TX) -> ExpectedID == ID andalso verify_signature_v2(TX, verify_signature) andalso verify_hash(TX). +%% @doc Generate the data segment to be signed for a given TX. +generate_signature_data_segment(#tx{ format = 2 } = TX) -> + case TX#tx.signature_type of + {?ECDSA_SIGN_ALG, secp256k1} -> + signature_data_segment_v2_no_public_key(TX); + {?RSA_SIGN_ALG, 65537} -> + signature_data_segment_v2(TX) + end; +generate_signature_data_segment(#tx{ format = 1 } = TX) -> + signature_data_segment_v1(TX). + tags_to_list(Tags) -> [[Name, Value] || {Name, Value} <- Tags]. @@ -253,6 +266,26 @@ signature_data_segment_v2(TX) -> end, ar_deep_hash:hash(List2). +signature_data_segment_v2_no_public_key(TX) -> + List = [ + << (integer_to_binary(TX#tx.format))/binary >>, + << (TX#tx.target)/binary >>, + << (list_to_binary(integer_to_list(TX#tx.quantity)))/binary >>, + << (list_to_binary(integer_to_list(TX#tx.reward)))/binary >>, + << (TX#tx.last_tx)/binary >>, + tags_to_list(TX#tx.tags), + << (integer_to_binary(TX#tx.data_size))/binary >>, + << (TX#tx.data_root)/binary >> + ], + List2 = + case TX#tx.denomination > 0 of + true -> + [<< (integer_to_binary(TX#tx.denomination))/binary >> | List]; + false -> + List + end, + ar_deep_hash:hash(List2). + %% @doc Generate the data segment to be signed for a given v1 TX. signature_data_segment_v1(TX) -> case TX#tx.denomination > 0 of @@ -284,8 +317,33 @@ sign(TX, PrivKey, {KeyType, Owner}, SignatureDataSegment) -> ID = crypto:hash(?HASH_ALG, <>), NewTX#tx{ id = ID, signature = Sig }. +verify_signature_type(#tx{ format = 1 } = TX, _Height) -> + case TX#tx.signature_type of + {?RSA_SIGN_ALG, 65537} -> + true; + _ -> + false + end; +verify_signature_type(#tx{ format = 2 } = TX, Height) -> + case TX#tx.signature_type of + {?RSA_SIGN_ALG, 65537} -> + true; + {?ECDSA_SIGN_ALG, secp256k1} -> + Height >= ar_fork:height_2_9(); + _ -> + false + end. + do_verify(#tx{ format = 1 } = TX, Args, VerifySignature) -> - do_verify_v1(TX, Args, VerifySignature); + {_Rate, _PricePerGiBMinute, _KryderPlusRateMultiplier, _Denomination, + _RedenominationHeight, Height, _Accounts, _Timestamp} = Args, + case verify_signature_type(TX, Height) of + true -> + do_verify_v1(TX, Args, VerifySignature); + false -> + collect_validation_results(TX#tx.id, + [{"tx_signature_type_not_supported", false}]) + end; do_verify(#tx{ format = 2 } = TX, Args, VerifySignature) -> {_Rate, _PricePerGiBMinute, _KryderPlusRateMultiplier, _Denomination, _RedenominationHeight, Height, _Accounts, _Timestamp} = Args, @@ -293,7 +351,13 @@ do_verify(#tx{ format = 2 } = TX, Args, VerifySignature) -> true -> collect_validation_results(TX#tx.id, [{"tx_format_not_supported", false}]); false -> - do_verify_v2(TX, Args, VerifySignature) + case verify_signature_type(TX, Height) of + true -> + do_verify_v2(TX, Args, VerifySignature); + false -> + collect_validation_results(TX#tx.id, + [{"tx_signature_type_not_supported", false}]) + end end; do_verify(TX, _Args, _VerifySignature) -> collect_validation_results(TX#tx.id, [{"tx_format_not_supported", false}]). @@ -414,13 +478,13 @@ verify_hash(#tx{ signature = Sig, id = ID }) -> verify_signature_v1(_TX, do_not_verify_signature) -> true; verify_signature_v1(TX, verify_signature) -> - SignatureDataSegment = signature_data_segment_v1(TX), + SignatureDataSegment = generate_signature_data_segment(TX), ar_wallet:verify({?DEFAULT_KEY_TYPE, TX#tx.owner}, SignatureDataSegment, TX#tx.signature). verify_signature_v1(_TX, do_not_verify_signature, _Height) -> true; verify_signature_v1(TX, verify_signature, Height) -> - SignatureDataSegment = signature_data_segment_v1(TX), + SignatureDataSegment = generate_signature_data_segment(TX), case Height >= ar_fork:height_2_4() of true -> ar_wallet:verify({?DEFAULT_KEY_TYPE, TX#tx.owner}, SignatureDataSegment, @@ -484,14 +548,20 @@ ends_with_digit(Data) -> verify_signature_v2(_TX, do_not_verify_signature) -> true; verify_signature_v2(TX = #tx{ signature_type = SigType }, verify_signature) -> - SignatureDataSegment = signature_data_segment_v2(TX), + SignatureDataSegment = generate_signature_data_segment(TX), ar_wallet:verify({SigType, TX#tx.owner}, SignatureDataSegment, TX#tx.signature). verify_signature_v2(_TX, do_not_verify_signature, _Height) -> true; verify_signature_v2(TX, verify_signature, Height) -> - SignatureDataSegment = signature_data_segment_v2(TX), - Wallet = {{?RSA_SIGN_ALG, 65537}, TX#tx.owner}, + SignatureDataSegment = generate_signature_data_segment(TX), + Wallet = + case TX#tx.signature_type of + ?RSA_KEY_TYPE -> + {{?RSA_SIGN_ALG, 65537}, TX#tx.owner}; + ?ECDSA_KEY_TYPE -> + {?ECDSA_KEY_TYPE, TX#tx.owner} + end, case Height >= ar_fork:height_2_4() of true -> ar_wallet:verify(Wallet, SignatureDataSegment, TX#tx.signature); diff --git a/apps/arweave/src/ar_wallet.erl b/apps/arweave/src/ar_wallet.erl index 452e05d72..669676a2e 100644 --- a/apps/arweave/src/ar_wallet.erl +++ b/apps/arweave/src/ar_wallet.erl @@ -1,14 +1,15 @@ %%% @doc Utilities for manipulating wallets. -module(ar_wallet). --export([new/0, new_ecdsa/0, new/1, sign/2, verify/3, verify_pre_fork_2_4/3, to_rsa_address/1, - to_address/1, to_address/2, load_key/1, load_keyfile/1, new_keyfile/0, new_keyfile/1, +-export([new/0, new_ecdsa/0, new/1, sign/2, verify/3, verify_pre_fork_2_4/3, + to_address/1, to_address/2, hash_pub_key/1, + load_key/1, load_keyfile/1, new_keyfile/0, new_keyfile/1, new_keyfile/2, base64_address_with_optional_checksum_to_decoded_address/1, base64_address_with_optional_checksum_to_decoded_address_safe/1, wallet_filepath/1, - get_or_create_wallet/1]). + get_or_create_wallet/1, recover_key/3]). --include_lib("arweave/include/ar.hrl"). --include_lib("arweave/include/ar_config.hrl"). +-include("../include/ar.hrl"). +-include("../include/ar_config.hrl"). -include_lib("public_key/include/public_key.hrl"). @@ -190,20 +191,9 @@ sign({{KeyAlg, PublicExpnt}, Priv, Pub}, Data) privateExponent = binary:decode_unsigned(Priv) } ); -sign({{KeyAlg, KeyCrv}, Priv, _} = Key, Data) +sign({{KeyAlg, KeyCrv}, Priv, _}, Data) when KeyAlg =:= ?ECDSA_SIGN_ALG andalso KeyCrv =:= secp256k1 -> - Sig = crypto:sign( - KeyAlg, - sha256, - Data, - [Priv, KeyCrv] - ), - case ecdsa_verify_low_s(Sig) of - true -> - Sig; - false -> - sign(Key, Data) - end; + secp256k1_nif:sign(Data, Priv); sign({{KeyAlg, KeyCrv}, Priv, _}, Data) when KeyAlg =:= ?EDDSA_SIGN_ALG andalso KeyCrv =:= ed25519 -> crypto:sign( @@ -225,24 +215,12 @@ verify({{KeyAlg, PublicExpnt}, Pub}, Data, Sig) modulus = binary:decode_unsigned(Pub) } ); +% NOTE. We will not write pubkey for ECDSA signature. So don't use verify function for ECDSA, use ecrecover +% So this function will return always false if called with no Pub verify({{KeyAlg, KeyCrv}, Pub}, Data, Sig) when KeyAlg =:= ?ECDSA_SIGN_ALG andalso KeyCrv =:= secp256k1 -> - case crypto:verify( - KeyAlg, - sha256, - Data, - Sig, - [Pub, KeyCrv] - ) of - false -> - false; - true -> - %% The measure against transaction malleability adopted from Ethereum. - %% All s-values greater than secp256k1n/2 are considered invalid. - %% Otherwise, one may flip the sign on both s and R to create the second - %% valid signature. - ecdsa_verify_low_s(Sig) - end; + {Pass, PubExtracted} = secp256k1_nif:ecrecover(Data, Sig), + Pass andalso PubExtracted =:= Pub; verify({{KeyAlg, KeyCrv}, Pub}, Data, Sig) when KeyAlg =:= ?EDDSA_SIGN_ALG andalso KeyCrv =:= ed25519 -> crypto:verify( @@ -283,12 +261,11 @@ to_address(PubKey, {?RSA_SIGN_ALG, 65537}) when bit_size(PubKey) == 256 -> %% Small keys are not secure, nobody is using them, the clause %% is for backwards-compatibility. PubKey; -to_address(PubKey, {?RSA_SIGN_ALG, 65537}) -> - to_rsa_address(PubKey); -to_address(PubKey, {?ECDSA_SIGN_ALG, secp256k1}) -> - << (?ECDSA_TYPE_BYTE)/binary, (hash_address(PubKey))/binary >>; -to_address(PubKey, {?EDDSA_SIGN_ALG, ed25519}) -> - << (?EDDSA_TYPE_BYTE)/binary, (hash_address(PubKey))/binary >>. +to_address(PubKey, _SigType) -> + hash_pub_key(PubKey). + +hash_pub_key(PubKey) -> + crypto:hash(?HASH_ALG, PubKey). base64_address_with_optional_checksum_to_decoded_address(AddrBase64) -> Size = byte_size(AddrBase64), @@ -354,6 +331,13 @@ get_or_create_wallet([{_LastModified, F} | Entries], Types) -> get_or_create_wallet(Entries, Types) end. +recover_key(Data, <<>>, ?ECDSA_KEY_TYPE) -> + <<>>; +recover_key(Data, Signature, ?ECDSA_KEY_TYPE) -> + {_Pass, PubKey} = secp256k1_nif:ecrecover(Data, Signature), + % Note. if Pass = false, then PubKey will be <<>> + PubKey. + %%%=================================================================== %%% Private functions. %%%=================================================================== @@ -366,20 +350,6 @@ wallet_name(wallet_address, PubKey, KeyType) -> wallet_name(WalletName, _, _) -> WalletName. -ecdsa_verify_low_s(Sig) -> - <<16#30, _Len0:8, 16#02, Len1:8, Rest/binary>> = Sig, - <<_R:Len1/binary, 16#02, _Len2:8, EncodedS/binary>> = Rest, - S = binary:decode_unsigned(EncodedS), - {_, _, _, EncodedOrder, _} = crypto:ec_curve(secp256k1), - Order = binary:decode_unsigned(EncodedOrder), - S < Order div 2 + 1. - -to_rsa_address(PubKey) -> - hash_address(PubKey). - -hash_address(PubKey) -> - crypto:hash(?HASH_ALG, PubKey). - decoded_address_to_checksum(AddrDecoded) -> Crc = erlang:crc32(AddrDecoded), << Crc:32 >>. diff --git a/apps/arweave/src/ar_wallets.erl b/apps/arweave/src/ar_wallets.erl index 32423dda3..3e0008749 100644 --- a/apps/arweave/src/ar_wallets.erl +++ b/apps/arweave/src/ar_wallets.erl @@ -319,7 +319,7 @@ apply_block2(B, PrevB, DAG) -> undefined -> Addresses2; Proof -> - [ar_wallet:to_address({?DEFAULT_KEY_TYPE, element(1, Proof)}) | Addresses2] + [ar_wallet:hash_pub_key(element(1, Proof)) | Addresses2] end, Accounts = get_map(Tree, Addresses3), case ar_node_utils:update_accounts(B, PrevB, Accounts) of diff --git a/apps/arweave/src/secp256k1_nif.erl b/apps/arweave/src/secp256k1_nif.erl new file mode 100755 index 000000000..1abe5804f --- /dev/null +++ b/apps/arweave/src/secp256k1_nif.erl @@ -0,0 +1,30 @@ +-module(secp256k1_nif). +-export([sign/2, ecrecover/2]). + +-on_load(init/0). + +-define(SigUpperBound, binary:decode_unsigned(<<16#7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0:256>>)). +-define(SigDiv, binary:decode_unsigned(<<16#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141:256>>)). + +init() -> + PrivDir = code:priv_dir(arweave), + ok = erlang:load_nif(filename:join([PrivDir, "secp256k1_arweave"]), 0). + +sign_recoverable(_Digest, _PrivateBytes) -> + erlang:nif_error(nif_not_loaded). + +recover_pk_and_verify(_Digest, _Signature) -> + erlang:nif_error(nif_not_loaded). + +sign(Msg, PrivBytes) -> + Digest = crypto:hash(sha256, Msg), + {ok, Signature} = sign_recoverable(Digest, PrivBytes), + Signature. + +ecrecover(Msg, Signature) -> + Digest = crypto:hash(sha256, Msg), + case recover_pk_and_verify(Digest, Signature) of + {ok, true, PubKey} -> {true, PubKey}; + {ok, false, _PubKey} -> {false, <<>>}; + {error, _Reason} -> {false, <<>>} + end. diff --git a/apps/arweave/test/ar_ecdsa_tests.erl b/apps/arweave/test/ar_ecdsa_tests.erl new file mode 100755 index 000000000..78b22f90f --- /dev/null +++ b/apps/arweave/test/ar_ecdsa_tests.erl @@ -0,0 +1,40 @@ +-module(ar_ecdsa_tests). + +-include("../include/ar.hrl"). + +-include_lib("eunit/include/eunit.hrl"). + +sign_ecrecover_test() -> + {{_, PrivBytes, PubBytes}, _} = ar_wallet:new({?ECDSA_SIGN_ALG, secp256k1}), + % Just call. It should not fail + ar_wallet:hash_pub_key(PubBytes), + Msg = <<"This is a test message!">>, + SigRecoverable = secp256k1_nif:sign(Msg, PrivBytes), + ?assertEqual(byte_size(SigRecoverable), 65), + + % recid byte + <> = SigRecoverable, + ?assert(lists:member(RecId, [0, 1, 2, 3])), + + % deterministic Sig + NewSig = secp256k1_nif:sign(Msg, PrivBytes), + ?assertEqual(NewSig, SigRecoverable), + + % Recover pk + {true, RecoveredBytes} = secp256k1_nif:ecrecover(Msg, SigRecoverable), + io:format("Prv ~p~n", [PrivBytes]), + io:format("Pub ~p~n", [PubBytes]), + ?assertEqual(RecoveredBytes, PubBytes), + + BadRecidSig = <>, + {false, <<>>} = secp256k1_nif:ecrecover(Msg, BadRecidSig), + + BadMsg = <<"This is a bad test message!">>, + {true, ArbitraryPubBytes1} = secp256k1_nif:ecrecover(BadMsg, SigRecoverable), + ?assertNotEqual(PubBytes, ArbitraryPubBytes1), + + % recover and verify returns true for arbitrary message, but non matching PK + {true, ArbitraryPubBytes2} = secp256k1_nif:ecrecover(crypto:strong_rand_bytes(100), SigRecoverable), + ?assertNotEqual(PubBytes, ArbitraryPubBytes2), + + ok. diff --git a/bin/create-ecdsa-wallet b/bin/create-ecdsa-wallet new file mode 100755 index 000000000..b0433db35 --- /dev/null +++ b/bin/create-ecdsa-wallet @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +SCRIPT_DIR="$(dirname "$0")" + +# Sets $ARWEAVE and $ARWEAVE_* variables +source $SCRIPT_DIR/arweave.env + +$ARWEAVE foreground $ARWEAVE_OPTS -run ar create_ecdsa_wallet "$@" + diff --git a/build_secp256k1.sh b/build_secp256k1.sh new file mode 100755 index 000000000..0c8a267e0 --- /dev/null +++ b/build_secp256k1.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +BUILD_DIR="apps/arweave/lib/secp256k1/build" +CMAKE_OPTIONS="-DSECP256K1_DISABLE_SHARED=ON \ + -DSECP256K1_ENABLE_MODULE_RECOVERY=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DSECP256K1_BUILD_BENCHMARK=OFF \ + -DSECP256K1_BUILD_EXHAUSTIVE_TESTS=OFF \ + -DSECP256K1_BUILD_TESTS=OFF \ + -DSECP256K1_ENABLE_MODULE_MUSIG=OFF \ + -DSECP256K1_ENABLE_MODULE_EXTRAKEYS=OFF \ + -DSECP256K1_ENABLE_MODULE_ELLSWIFT=OFF \ + -DSECP256K1_ENABLE_MODULE_SCHNORRSIG=OFF \ + -DSECP256K1_APPEND_CFLAGS=-fPIC" +mkdir -p $BUILD_DIR +cd $BUILD_DIR +cmake $CMAKE_OPTIONS .. +cmake --build . diff --git a/rebar.config b/rebar.config index 7c72e8ca0..18c3477e0 100644 --- a/rebar.config +++ b/rebar.config @@ -398,6 +398,8 @@ {"(linux)", compile, "make -C apps/arweave/lib/RandomX/buildsquared"}, {"(freebsd|netbsd|openbsd)", compile, "gmake -C apps/arweave/lib/RandomX/buildsquared"}, {"(darwin|linux|freebsd|netbsd|openbsd)", compile, "bash -c \"cd apps/arweave/lib/RandomX/buildsquared && mv librandomx.a librandomxsquared.a\""}, + % Build libsecp256k1 + {"(darwin|linux|freebsd|netbsd|openbsd)", compile, "./build_secp256k1.sh > /dev/null"}, % Compile NIFs {"(linux)", compile, "env AR=gcc-ar make all -C apps/arweave/c_src"}, {"(darwin)", compile, "make all -C apps/arweave/c_src"}, @@ -413,6 +415,8 @@ % Clean randomxsquared {"(linux|darwin)", clean, "bash -c \"if [ -d apps/arweave/lib/RandomX/buildsquared ]; then make -C apps/arweave/lib/RandomX/buildsquared clean; fi\""}, {"(freebsd|netbsd|openbsd)", clean, "bash -c \"if [ -d apps/arweave/lib/RandomX/buildsquared ]; then gmake -C apps/arweave/lib/RandomX/buildsquared clean; fi\""}, + % Clean secp256k1 + {"(linux|darwin|freebsd|netbsd|openbsd)", clean, "bash -c \"if [ -d apps/arweave/lib/secp256k1/build ]; then rm -rf apps/arweave/lib/secp256k1/build; fi\""}, % Clan NIFs {"(linux|darwin)", clean, "make -C apps/arweave/c_src clean"}, {"(freebsd|netbsd|openbsd)", clean, "gmake -C apps/arweave/c_src clean"}