Skip to content

Commit

Permalink
EIP1559 New fee support (#1509)
Browse files Browse the repository at this point in the history
* Add new fields to Ethereum proto.

* Legacy transaction building.

* Typescript test update

* TS test fix

* Compiler warning fix (ios)

* Legacy and Enveloped transactions.

* lSigner.
Refactor Transaction

* AccessList generation test (wip)

* Cleanup, small refactor

* Signer build change, make it more generic.

* Copyright year

* SignatureRSV rename.

* Signer class is static now, no chainID memeber.

* Renames, Legacy

* Minor touches

* Rename, hash

* Minor reorg in sign()

* Eip1559 tx implementation (native transfer only).

* Additional EIP1559 build methods

* Revert renumbering in proto file (to be bw compatible)

* Additional tests for 1559-fee'd ERC20 and other 3 messages

* Test fix

* iOS test data fix

* Android test fix

* Update coverage (94.6%)

* Merge fix

* Merge fix

* Add comment on replay protection

* Empty (access) list as const.

Co-authored-by: Catenocrypt <[email protected]>
Co-authored-by: hewigovens <[email protected]>
  • Loading branch information
3 people authored Aug 5, 2021
1 parent 11c689f commit 1e34cc2
Show file tree
Hide file tree
Showing 10 changed files with 494 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,30 @@ class TestEthereumTransactionSigner {
assertEquals(Numeric.toHexString(output.data.toByteArray()), "0xa9059cbb0000000000000000000000005322b34c88ed0691971bf52a7047448f0f4efc840000000000000000000000000000000000000000000000001bc16d674ec80000")
}

@Test
fun testEthereumERC20_1559_Signing() {
val signingInput = Ethereum.SigningInput.newBuilder()
signingInput.apply {
privateKey = ByteString.copyFrom(PrivateKey("0x608dcb1742bb3fb7aec002074e3420e4fab7d00cced79ccdac53ed5b27138151".toHexByteArray()).data())
toAddress = "0x6b175474e89094c44da98b954eedeac495271d0f" // DAI
chainId = ByteString.copyFrom("0x1".toHexByteArray())
nonce = ByteString.copyFrom("0x0".toHexByteArray())
maxInclusionFeePerGas = ByteString.copyFrom("0x77359400".toHexByteArray()) // 2000000000
maxFeePerGas = ByteString.copyFrom("0xB2D05E00".toHexByteArray()) // 3000000000
gasLimit = ByteString.copyFrom("0x0130B9".toHexByteArray())
transaction = Ethereum.Transaction.newBuilder().apply {
erc20Transfer = Ethereum.Transaction.ERC20Transfer.newBuilder().apply {
to = "0x5322b34c88ed0691971bf52a7047448f0f4efc84"
amount = ByteString.copyFrom("0x1bc16d674ec80000".toHexByteArray())
}.build()
}.build()
}

val output = AnySigner.sign(signingInput.build(), ETHEREUM, SigningOutput.parser())

assertEquals(Numeric.toHexString(output.encoded.toByteArray()), "0x02f8b00180847735940084b2d05e00830130b9946b175474e89094c44da98b954eedeac495271d0f80b844a9059cbb0000000000000000000000005322b34c88ed0691971bf52a7047448f0f4efc840000000000000000000000000000000000000000000000001bc16d674ec80000c080a0adfcfdf98d4ed35a8967a0c1d78b42adb7c5d831cf5a3272654ec8f8bcd7be2ea011641e065684f6aa476f4fd250aa46cd0b44eccdb0a6e1650d658d1998684cdf")
}

@Test
fun testEthereumERC721Signing() {
val signingInput = Ethereum.SigningInput.newBuilder()
Expand Down
116 changes: 82 additions & 34 deletions src/Ethereum/Signer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ Proto::SigningOutput Signer::sign(const Proto::SigningInput& input) noexcept {
auto key = PrivateKey(Data(input.private_key().begin(), input.private_key().end()));
auto transaction = Signer::build(input);

auto preHash = transaction->preHash(chainID);
auto signature = sign(key, chainID, preHash);
auto signature = sign(key, chainID, transaction);

auto output = Proto::SigningOutput();

Expand All @@ -27,10 +26,8 @@ Proto::SigningOutput Signer::sign(const Proto::SigningInput& input) noexcept {

auto v = store(signature.v);
output.set_v(v.data(), v.size());

auto r = store(signature.r);
output.set_r(r.data(), r.size());

auto s = store(signature.s);
output.set_s(s.data(), s.size());

Expand All @@ -50,26 +47,31 @@ std::string Signer::signJSON(const std::string& json, const Data& key) {
return hex(output.encoded());
}

Signature Signer::valuesRSV(const uint256_t& chainID, const Data& signature) noexcept {
Signature Signer::signatureDataToStruct(const Data& signature) noexcept {
boost::multiprecision::uint256_t r, s, v;
import_bits(r, signature.begin(), signature.begin() + 32);
import_bits(s, signature.begin() + 32, signature.begin() + 64);
import_bits(v, signature.begin() + 64, signature.begin() + 65);
v += 27;
return Signature{r, s, v};
}

boost::multiprecision::uint256_t newV;
Signature Signer::signatureDataToStructWithEip155(const uint256_t& chainID, const Data& signature) noexcept {
Signature rsv = signatureDataToStruct(signature);
// Embed chainID in V param, for replay protection, legacy (EIP155)
if (chainID != 0) {
import_bits(newV, signature.begin() + 64, signature.begin() + 65);
newV += 35 + chainID + chainID;
rsv.v += 35 + chainID + chainID;
} else {
newV = v;
rsv.v += 27;
}
return Signature{r, s, newV};
return rsv;
}

Signature Signer::sign(const PrivateKey& privateKey, const uint256_t& chainID, const Data& hash) noexcept {
Signature Signer::sign(const PrivateKey& privateKey, const Data& hash, bool includeEip155, const uint256_t& chainID) noexcept {
auto signature = privateKey.sign(hash, TWCurveSECP256k1);
return valuesRSV(chainID, signature);
if (!includeEip155) {
return signatureDataToStruct(signature);
}
return signatureDataToStructWithEip155(chainID, signature);
}

// May throw
Expand All @@ -84,88 +86,134 @@ Data addressStringToData(const std::string& asString) {
return asData;
}

std::shared_ptr<TransactionNonTyped> Signer::buildNonTyped(const Proto::SigningInput& input) {
std::shared_ptr<TransactionBase> Signer::build(const Proto::SigningInput& input) {
Data toAddress = addressStringToData(input.to_address());
uint256_t nonce = load(input.nonce());
uint256_t gasPrice = load(input.gas_price());
uint256_t gasLimit = load(input.gas_limit());
assert(gasPrice != 0);
uint256_t maxInclusionFeePerGas = load(input.max_inclusion_fee_per_gas());
uint256_t maxFeePerGas = load(input.max_fee_per_gas());
switch (input.transaction().transaction_oneof_case()) {
case Proto::Transaction::kTransfer:
{
auto transaction = TransactionNonTyped::buildNativeTransfer(
nonce, gasPrice, gasLimit,
if (gasPrice != 0) { // legacy
return TransactionNonTyped::buildNativeTransfer(
nonce, gasPrice, gasLimit,
/* to: */ toAddress,
/* amount: */ load(input.transaction().transfer().amount()),
/* optional data: */ Data(input.transaction().transfer().data().begin(), input.transaction().transfer().data().end()));
}
// Eip1559
return TransactionEip1559::buildNativeTransfer(
nonce, maxInclusionFeePerGas, maxFeePerGas, gasLimit,
/* to: */ toAddress,
/* amount: */ load(input.transaction().transfer().amount()),
/* optional data: */ Data(input.transaction().transfer().data().begin(), input.transaction().transfer().data().end()));
return transaction;
}

case Proto::Transaction::kErc20Transfer:
{
Data tokenToAddress = addressStringToData(input.transaction().erc20_transfer().to());
auto transaction = TransactionNonTyped::buildERC20Transfer(
nonce, gasPrice, gasLimit,
if (gasPrice != 0) { // legacy
return TransactionNonTyped::buildERC20Transfer(
nonce, gasPrice, gasLimit,
/* tokenContract: */ toAddress,
/* toAddress */ tokenToAddress,
/* amount: */ load(input.transaction().erc20_transfer().amount()));
}
// Eip1559
return TransactionEip1559::buildERC20Transfer(
nonce, maxInclusionFeePerGas, maxFeePerGas, gasLimit,
/* tokenContract: */ toAddress,
/* toAddress */ tokenToAddress,
/* amount: */ load(input.transaction().erc20_transfer().amount()));
return transaction;
}

case Proto::Transaction::kErc20Approve:
{
Data spenderAddress = addressStringToData(input.transaction().erc20_approve().spender());
auto transaction = TransactionNonTyped::buildERC20Approve(
nonce, gasPrice, gasLimit,
if (gasPrice != 0) { // legacy
return TransactionNonTyped::buildERC20Approve(
nonce, gasPrice, gasLimit,
/* tokenContract: */ toAddress,
/* toAddress */ spenderAddress,
/* amount: */ load(input.transaction().erc20_approve().amount()));
}
// Eip1559
return TransactionEip1559::buildERC20Approve(
nonce, maxInclusionFeePerGas, maxFeePerGas, gasLimit,
/* tokenContract: */ toAddress,
/* toAddress */ spenderAddress,
/* amount: */ load(input.transaction().erc20_approve().amount()));
return transaction;
}

case Proto::Transaction::kErc721Transfer:
{
Data tokenToAddress = addressStringToData(input.transaction().erc721_transfer().to());
Data tokenFromAddress = addressStringToData(input.transaction().erc721_transfer().from());
auto transaction = TransactionNonTyped::buildERC721Transfer(
nonce, gasPrice, gasLimit,
if (gasPrice != 0) { // legacy
return TransactionNonTyped::buildERC721Transfer(
nonce, gasPrice, gasLimit,
/* tokenContract: */ toAddress,
/* fromAddress: */ tokenFromAddress,
/* toAddress */ tokenToAddress,
/* tokenId: */ load(input.transaction().erc721_transfer().token_id()));
}
// Eip1559
return TransactionEip1559::buildERC721Transfer(
nonce, maxInclusionFeePerGas, maxFeePerGas, gasLimit,
/* tokenContract: */ toAddress,
/* fromAddress: */ tokenFromAddress,
/* toAddress */ tokenToAddress,
/* tokenId: */ load(input.transaction().erc721_transfer().token_id()));
return transaction;
}

case Proto::Transaction::kErc1155Transfer:
{
Data tokenToAddress = addressStringToData(input.transaction().erc1155_transfer().to());
Data tokenFromAddress = addressStringToData(input.transaction().erc1155_transfer().from());
auto transaction = TransactionNonTyped::buildERC1155Transfer(
nonce, gasPrice, gasLimit,
if (gasPrice != 0) { // legacy
return TransactionNonTyped::buildERC1155Transfer(
nonce, gasPrice, gasLimit,
/* tokenContract: */ toAddress,
/* fromAddress: */ tokenFromAddress,
/* toAddress */ tokenToAddress,
/* tokenId: */ load(input.transaction().erc1155_transfer().token_id()),
/* value */ load(input.transaction().erc1155_transfer().value()),
/* data */ Data(input.transaction().erc1155_transfer().data().begin(), input.transaction().erc1155_transfer().data().end())
);
}
return TransactionEip1559::buildERC1155Transfer(
nonce, maxInclusionFeePerGas, maxFeePerGas, gasLimit,
/* tokenContract: */ toAddress,
/* fromAddress: */ tokenFromAddress,
/* toAddress */ tokenToAddress,
/* tokenId: */ load(input.transaction().erc1155_transfer().token_id()),
/* value */ load(input.transaction().erc1155_transfer().value()),
/* data */ Data(input.transaction().erc1155_transfer().data().begin(), input.transaction().erc1155_transfer().data().end())
);
return transaction;
}

case Proto::Transaction::kContractGeneric:
default:
{
auto transaction = TransactionNonTyped::buildNativeTransfer(
nonce, gasPrice, gasLimit,
if (gasPrice != 0) { // legacy
return TransactionNonTyped::buildNativeTransfer(
nonce, gasPrice, gasLimit,
/* to: */ toAddress,
/* amount: */ load(input.transaction().contract_generic().amount()),
/* transaction: */ Data(input.transaction().contract_generic().data().begin(), input.transaction().contract_generic().data().end()));
}
return TransactionEip1559::buildNativeTransfer(
nonce, maxInclusionFeePerGas, maxFeePerGas, gasLimit,
/* to: */ toAddress,
/* amount: */ load(input.transaction().contract_generic().amount()),
/* transaction: */ Data(input.transaction().contract_generic().data().begin(), input.transaction().contract_generic().data().end()));
return transaction;
}
}
}

Signature Signer::sign(const PrivateKey& privateKey, const uint256_t& chainID, std::shared_ptr<TransactionBase> transaction) noexcept {
auto preHash = transaction->preHash(chainID);
return Signer::sign(privateKey, chainID, preHash);
return Signer::sign(privateKey, preHash, transaction->usesReplayProtection(), chainID);
}
14 changes: 8 additions & 6 deletions src/Ethereum/Signer.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,20 @@ class Signer {

public:
/// build Transaction from signing input
static std::shared_ptr<TransactionBase> build(const Proto::SigningInput& input) { return buildNonTyped(input); }
static std::shared_ptr<TransactionNonTyped> buildNonTyped(const Proto::SigningInput& input);
static std::shared_ptr<TransactionBase> build(const Proto::SigningInput& input);

/// Signs a hash with the given private key for the given chain identifier.
///
/// @returns the r, s, and v values of the transaction signature
static Signature sign(const PrivateKey& privateKey, const uint256_t& chainID, const Data& hash) noexcept;
static Signature sign(const PrivateKey& privateKey, const Data& hash, bool includeEip155, const uint256_t& chainID) noexcept;

/// R, S, and V values for the given chain identifier and signature.
///
/// Break up the signature into the R, S, and V values.
/// @returns the r, s, and v values of the transaction signature
static Signature signatureDataToStruct(const Data& signature) noexcept;

/// Break up the signature into the R, S, and V values, and include chainID in V for replay protection (Eip155)
/// @returns the r, s, and v values of the transaction signature
static Signature valuesRSV(const uint256_t& chainID, const Data& signature) noexcept;
static Signature signatureDataToStructWithEip155(const uint256_t& chainID, const Data& signature) noexcept;
};

} // namespace TW::Ethereum
Expand Down
Loading

0 comments on commit 1e34cc2

Please sign in to comment.