Skip to content

Commit

Permalink
CAP-0027 Add support for muxed accounts (#96)
Browse files Browse the repository at this point in the history
* Bump to v0.5.0 stellar_base dep

* Add new keypair functions to handle muxed accounts

* Improve AccountID. Add descriptive error messages

* CAP-0027 Add support for muxed accounts

* Fix code formatting

* Use muxed_id instead of id in the Account struct

* Improve module description
  • Loading branch information
Juan Hurtado authored Feb 10, 2022
1 parent 5c80123 commit e4e8180
Show file tree
Hide file tree
Showing 24 changed files with 463 additions and 74 deletions.
13 changes: 13 additions & 0 deletions lib/keypair.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ defmodule Stellar.KeyPair do
@impl true
def from_secret_seed(secret), do: impl().from_secret_seed(secret)

@impl true
def from_raw_public_key(raw_public_key), do: impl().from_raw_public_key(raw_public_key)

@impl true
def from_raw_muxed_account(raw_muxed_account),
do: impl().from_raw_muxed_account(raw_muxed_account)

@impl true
def raw_public_key(public_key), do: impl().raw_public_key(public_key)

@impl true
def raw_muxed_account(public_key), do: impl().raw_muxed_account(public_key)

@impl true
def raw_secret_seed(secret), do: impl().raw_secret_seed(secret)

Expand All @@ -25,6 +35,9 @@ defmodule Stellar.KeyPair do
@impl true
def validate_public_key(public_key), do: impl().validate_public_key(public_key)

@impl true
def validate_muxed_account(public_key), do: impl().validate_muxed_account(public_key)

@impl true
def validate_secret_seed(secret), do: impl().validate_secret_seed(secret)

Expand Down
31 changes: 25 additions & 6 deletions lib/keypair/default.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ defmodule Stellar.KeyPair.Default do
{public_key, secret}
end

@impl true
def from_raw_public_key(public_key) do
StrKey.encode!(public_key, :ed25519_public_key)
end

@impl true
def from_raw_muxed_account(muxed_account) do
StrKey.encode!(muxed_account, :muxed_account)
end

@impl true
def raw_public_key(public_key) do
StrKey.decode!(public_key, :ed25519_public_key)
Expand All @@ -36,6 +46,11 @@ defmodule Stellar.KeyPair.Default do
StrKey.decode!(secret, :ed25519_secret_seed)
end

@impl true
def raw_muxed_account(muxed_account) do
StrKey.decode!(muxed_account, :muxed_account)
end

@impl true
def sign(<<payload::binary>>, <<secret::binary>>) do
raw_secret = raw_secret_seed(secret)
Expand All @@ -45,22 +60,26 @@ defmodule Stellar.KeyPair.Default do
def sign(_payload, _secret), do: {:error, :invalid_signature_payload}

@impl true
def validate_public_key(public_key) when byte_size(public_key) == 56 do
def validate_public_key(public_key) do
case StrKey.decode(public_key, :ed25519_public_key) do
{:ok, _key} -> :ok
{:error, _reason} -> {:error, :invalid_ed25519_public_key}
end
end

def validate_public_key(_public_key), do: {:error, :invalid_ed25519_public_key}
@impl true
def validate_muxed_account(muxed_account) do
case StrKey.decode(muxed_account, :muxed_account) do
{:ok, _key} -> :ok
{:error, _reason} -> {:error, :invalid_ed25519_muxed_account}
end
end

@impl true
def validate_secret_seed(secret) when byte_size(secret) == 56 do
def validate_secret_seed(secret) do
case StrKey.decode(secret, :ed25519_secret_seed) do
{:ok, _secret} -> :ok
{:ok, _key} -> :ok
{:error, _reason} -> {:error, :invalid_ed25519_secret_seed}
end
end

def validate_secret_seed(_secret), do: {:error, :invalid_ed25519_secret_seed}
end
20 changes: 12 additions & 8 deletions lib/keypair/spec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ defmodule Stellar.KeyPair.Spec do
@type validation :: :ok | error()

@callback random() :: {public_key(), secret_seed()}

@callback from_secret_seed(secret_seed()) :: {public_key(), secret_seed()}

@callback from_raw_public_key(binary()) :: public_key()
@callback from_raw_muxed_account(binary()) :: public_key()
@callback raw_public_key(public_key()) :: binary()

@callback raw_muxed_account(public_key()) :: binary()
@callback raw_secret_seed(public_key()) :: binary()

@callback validate_public_key(String.t()) :: validation()

@callback validate_secret_seed(String.t()) :: validation()

@callback validate_public_key(public_key()) :: validation()
@callback validate_muxed_account(public_key()) :: validation()
@callback validate_secret_seed(public_key()) :: validation()
@callback sign(binary(), secret_seed()) :: binary() | error()

@optional_callbacks from_raw_public_key: 1,
from_raw_muxed_account: 1,
validate_public_key: 1,
validate_muxed_account: 1,
validate_secret_seed: 1
end
113 changes: 98 additions & 15 deletions lib/tx_build/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,123 @@ defmodule Stellar.TxBuild.Account do
`Account` struct definition.
"""
alias Stellar.KeyPair
alias StellarBase.XDR.{CryptoKeyType, MuxedAccount, UInt256}
alias StellarBase.XDR.{CryptoKeyType, MuxedAccount, MuxedAccountMed25519, UInt64, UInt256}

@behaviour Stellar.TxBuild.XDR

@type address :: String.t()
@type account_id :: String.t()
@type validation :: {:ok, account_id()} | {:error, atom()}
@type muxed_id :: integer() | nil
@type type :: :ed25519_public_key | :ed25519_muxed_account
@type error :: {:error, atom()}
@type validation :: {:ok, any()} | error()

@type t :: %__MODULE__{account_id: account_id()}
@type t :: %__MODULE__{
address: address(),
account_id: account_id(),
muxed_id: muxed_id(),
type: type()
}

defstruct [:account_id]
defstruct [:address, :account_id, :muxed_id, :type]

@impl true
def new(account_id, opts \\ [])
def new(address, opts \\ [])

def new(account_id, _opts) do
with {:ok, account_id} <- validate_raw_account_id(account_id),
do: %__MODULE__{account_id: account_id}
def new(address, _opts) when byte_size(address) == 69 do
with {:ok, address} <- validate_muxed_address(address),
{:ok, {account_id, muxed_id}} <- parse_muxed_address(address) do
%__MODULE__{
address: address,
account_id: account_id,
muxed_id: muxed_id,
type: :ed25519_muxed_account
}
end
end

def new(address, _opts) when byte_size(address) == 56 do
case validate_public_key(address) do
{:ok, account_id} ->
%__MODULE__{
address: account_id,
account_id: account_id,
type: :ed25519_public_key
}

error ->
error
end
end

def new(_address, _opts), do: {:error, :invalid_ed25519_public_key}

@impl true
def to_xdr(%__MODULE__{account_id: account_id, muxed_id: muxed_id, type: :ed25519_muxed_account}) do
type = CryptoKeyType.new(:KEY_TYPE_MUXED_ED25519)
ed25519_public_key_xdr = ed25519_public_key_xdr(account_id)

muxed_id
|> UInt64.new()
|> MuxedAccountMed25519.new(ed25519_public_key_xdr)
|> MuxedAccount.new(type)
end

def to_xdr(%__MODULE__{account_id: account_id}) do
type = CryptoKeyType.new(:KEY_TYPE_ED25519)
ed25519_public_key_xdr = ed25519_public_key_xdr(account_id)

MuxedAccount.new(ed25519_public_key_xdr, type)
end

@spec create_muxed(account_id :: account_id(), muxed_id :: muxed_id()) :: t()
def create_muxed(account_id, muxed_id)
when byte_size(account_id) == 56 and is_integer(muxed_id) do
account_id
|> KeyPair.raw_public_key()
|> UInt256.new()
|> MuxedAccount.new(type)
|> Kernel.<>(<<muxed_id::big-unsigned-integer-size(64)>>)
|> KeyPair.from_raw_muxed_account()
|> new()
end

@spec validate_raw_account_id(account_id :: String.t()) :: validation()
defp validate_raw_account_id(account_id) do
case KeyPair.validate_public_key(account_id) do
:ok -> {:ok, account_id}
_error -> {:error, :invalid_account_id}
def create_muxed(_account_id, _id), do: {:error, :invalid_muxed_account}

@spec validate_muxed_address(address :: address()) :: validation()
defp validate_muxed_address(address) do
case KeyPair.validate_muxed_account(address) do
:ok -> {:ok, address}
error -> error
end
end

@spec validate_public_key(address :: address()) :: validation()
defp validate_public_key(address) do
case KeyPair.validate_public_key(address) do
:ok -> {:ok, address}
error -> error
end
end

@spec parse_muxed_address(address :: address()) :: {:ok, {account_id(), muxed_id()}} | error()
defp parse_muxed_address(address) do
address
|> KeyPair.raw_muxed_account()
|> encode_muxed_address()
end

@spec encode_muxed_address(decoded_address :: binary()) ::
{:ok, {account_id(), muxed_id()}} | error()
defp encode_muxed_address(<<decoded::binary-size(32), muxed_id::big-unsigned-integer-size(64)>>) do
ed25519_key = KeyPair.from_raw_public_key(decoded)
{:ok, {ed25519_key, muxed_id}}
end

defp encode_muxed_address(_muxed_account), do: {:error, :invalid_muxed_address}

@spec ed25519_public_key_xdr(account_id :: account_id()) :: UInt256.t()
defp ed25519_public_key_xdr(account_id) do
account_id
|> KeyPair.raw_public_key()
|> UInt256.new()
end
end
14 changes: 4 additions & 10 deletions lib/tx_build/account_id.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ defmodule Stellar.TxBuild.AccountID do
def new(account_id, opts \\ [])

def new(account_id, _opts) do
with {:ok, account_id} <- validate_raw_account_id(account_id),
do: %__MODULE__{account_id: account_id}
case KeyPair.validate_public_key(account_id) do
:ok -> %__MODULE__{account_id: account_id}
error -> error
end
end

@impl true
Expand All @@ -32,12 +34,4 @@ defmodule Stellar.TxBuild.AccountID do
|> PublicKey.new(type)
|> AccountID.new()
end

@spec validate_raw_account_id(account_id :: String.t()) :: validation()
defp validate_raw_account_id(account_id) do
case KeyPair.validate_public_key(account_id) do
:ok -> {:ok, account_id}
_error -> {:error, :invalid_account_id}
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ defmodule Stellar.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:stellar_base, "~> 0.3.0"},
{:stellar_base, "~> 0.5.0"},
{:ed25519, "~> 1.3"},
{:hackney, "~> 1.17", optional: true},
{:jason, "~> 1.0", optional: true},
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
"crc": {:hex, :crc, "0.10.2", "93ee6788904735d4d93f59a1e80860e4c9aa44e8d2ff7c69857eb62757454137", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "6b931cfb5e7d20c3c4113adab460f29ee5a50a36b397edd81c9bede2bbdb505c"},
"crc": {:hex, :crc, "0.10.4", "06f5f54e2ec2954968703dcd37d7a4c65cee7a5305c48a23c509dc20a5469d4f", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "90bdd5b5ac883f0b1860692324ed3f53fdaa6f1e8483771873fea07e71def91d"},
"credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"},
Expand All @@ -24,6 +24,6 @@
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"stellar_base": {:hex, :stellar_base, "0.3.0", "21e0253044ba2bcb86f1af92e2847543b42c40cac0863318a22d5a252814d799", [:mix], [{:crc, "~> 0.10.0", [hex: :crc, repo: "hexpm", optional: false]}, {:elixir_xdr, "~> 0.2.0", [hex: :elixir_xdr, repo: "hexpm", optional: false]}], "hexpm", "9f53a22277842a2fc70d5544944bd04a8cd9e00b4cfb5d627887cb30313eff5d"},
"stellar_base": {:hex, :stellar_base, "0.5.0", "41dab1d99b86f7ee8a86d07d1ddc59234f3be1df6a327479a7225b347425f546", [:mix], [{:crc, "~> 0.10.0", [hex: :crc, repo: "hexpm", optional: false]}, {:elixir_xdr, "~> 0.2.0", [hex: :elixir_xdr, repo: "hexpm", optional: false]}], "hexpm", "fa9f5c9bcc8607f2b80b49791541fab3835442f89b98f848bd86744873862cdc"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}
33 changes: 33 additions & 0 deletions test/keypair/keypair_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ defmodule Stellar.KeyPair.CannedKeyPairImpl do
:ok
end

@impl true
def from_raw_public_key(_public_key) do
send(self(), {:from_raw_public_key, "PUBLIC_KEY"})
:ok
end

@impl true
def raw_public_key(_public_key) do
send(self(), {:raw_public_key, "RAW_PUBLIC_KEY"})
Expand All @@ -27,6 +33,12 @@ defmodule Stellar.KeyPair.CannedKeyPairImpl do
:ok
end

@impl true
def raw_muxed_account(_muxed_account) do
send(self(), {:raw_muxed_account, "RAW_MUXED_ACCOUNT"})
:ok
end

@impl true
def sign(_payload, _secret) do
send(self(), {:signature, "SIGNATURE"})
Expand All @@ -44,6 +56,12 @@ defmodule Stellar.KeyPair.CannedKeyPairImpl do
send(self(), {:ok, "SECRET_SEED"})
:ok
end

@impl true
def validate_muxed_account(_muxed_account) do
send(self(), {:validate_muxed_account, "MUXED_ACCOUNT"})
:ok
end
end

defmodule Stellar.KeyPairTest do
Expand All @@ -69,6 +87,11 @@ defmodule Stellar.KeyPairTest do
assert_receive({:secret, "SECRET"})
end

test "from_raw_public_key/2" do
Stellar.KeyPair.from_raw_public_key("RAW_PUBLIC_KEY")
assert_receive({:from_raw_public_key, "PUBLIC_KEY"})
end

test "raw_public_key/1" do
Stellar.KeyPair.raw_public_key("RAW_PUBLIC_KEY")
assert_receive({:raw_public_key, "RAW_PUBLIC_KEY"})
Expand All @@ -79,6 +102,11 @@ defmodule Stellar.KeyPairTest do
assert_receive({:raw_secret, "RAW_SECRET"})
end

test "raw_muxed_account/2" do
Stellar.KeyPair.raw_muxed_account("MUXED_ACCOUNT")
assert_receive({:raw_muxed_account, "RAW_MUXED_ACCOUNT"})
end

test "sign/2" do
Stellar.KeyPair.sign(<<0, 0, 0, 0>>, "SECRET")
assert_receive({:signature, "SIGNATURE"})
Expand All @@ -93,4 +121,9 @@ defmodule Stellar.KeyPairTest do
Stellar.KeyPair.validate_secret_seed("SECRET_SEED")
assert_receive({:ok, "SECRET_SEED"})
end

test "validate_muxed_account/2" do
Stellar.KeyPair.validate_muxed_account("MUXED_ACCOUNT")
assert_receive({:validate_muxed_account, "MUXED_ACCOUNT"})
end
end
Loading

0 comments on commit e4e8180

Please sign in to comment.