Skip to content

Commit

Permalink
Support authorized invocation with different accounts (#321)
Browse files Browse the repository at this point in the history
* Add sign functions for contract invoke function op's

* Documentation update

* Add minor changes

* Add requested changes

---------

Co-authored-by: Edwin Steven Guayacan <EdwinGuayacan@users.noreply.github.com>
  • Loading branch information
Odraxs and EdwinGuayacan authored Aug 1, 2023
1 parent f0bfc32 commit b5a436a
Show file tree
Hide file tree
Showing 6 changed files with 368 additions and 90 deletions.
101 changes: 49 additions & 52 deletions docs/examples/soroban/invoke_contract_functions.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Invoke Contract Function

> **Warning**
> Please note that Soroban is still under development, so breaking changes may occur.
Expand All @@ -9,6 +10,9 @@ There are three ways to perform a contract function invocation:

### Without Contract Authorization

> **Note**
> Used to invoke functions that don't require any authorization.
```elixir
alias Stellar.TxBuild.{
Account,
Expand Down Expand Up @@ -66,6 +70,9 @@ source_account

### With Authorization

> **Note**
> Used when the tx submitter and the invoker are the same.
```elixir
alias Stellar.TxBuild.{
InvokeHostFunction,
Expand Down Expand Up @@ -133,19 +140,18 @@ source_account
|> Stellar.TxBuild.envelope()
```

### With Stellar Account Authorization (WIP: Preview 10 support)
### With Stellar Account Authorization

> **Note**
> Used when the tx submitter is different from the invoker.
```elixir
alias StellarBase.XDR.{SorobanResources, SorobanTransactionData, UInt32}

alias Stellar.TxBuild.{
ContractAuth,
AddressWithNonce,
AuthorizedInvocation,
BaseFee,
InvokeHostFunction,
HostFunction,
HostFunctionArgs,
SCVal,
SCAddress,
SequenceNumber,
Expand All @@ -155,59 +161,42 @@ alias Stellar.TxBuild.{
alias Stellar.Horizon.Accounts
alias Stellar.KeyPair

contract_id = "8367a1324fdbb56d41e6a6cea2364e389e9f4e17d3ebea810d7bdeca663c2cd5"
function_name = "inc"
contract_address =
"CAMGSYINVVL6WP3Q5WPNL7FS4GZP37TWV7MKIRQF5QMYLK3N2SW4P3RC"
|> SCAddress.new()
|> (&SCVal.new(address: &1)).()
function_name = SCVal.new(symbol: "inc")

## invoker
{public_key, secret_key} =
{invoker_public_key, invoker_secret_key} =
KeyPair.from_secret_seed("SCAVFA3PI3MJLTQNMXOUNBSEUOSY66YMG3T2KCQKLQBENNVLVKNPV3EK")

## submitter
keypair2 =
{public_key_2, _secret_key_2} =
submitter_keypair =
{submitter_public_key, _submitter_secret_key} =
KeyPair.from_secret_seed("SDRD4CSRGPWUIPRDS5O3CJBNJME5XVGWNI677MZDD4OD2ZL2R6K5IQ24")

address_type = SCAddress.new(public_key)
address_type = SCAddress.new(invoker_public_key)
address = SCVal.new(address: address_type)
args = [address, SCVal.new(u128: %{hi: 0, lo: 2})]

function_args =
HostFunctionArgs.new(
type: :invoke,
contract_id: contract_id,
function_name: function_name,
args: args
)

auth_invocation =
AuthorizedInvocation.new(
contract_id: contract_id,
function_name: function_name,
args: args,
sub_invocations: []
)

# Nonce increment by 1 each successfully contract call
address_with_nonce = AddressWithNonce.new(address: address_type, nonce: 0)

contract_auth =
ContractAuth.new(
address_with_nonce: address_with_nonce,
authorized_invocation: auth_invocation
)
|> ContractAuth.sign(secret_key)

function = HostFunction.new(args: function_args, auth: [contract_auth])

invoke_host_function_op =
InvokeHostFunction.new(functions: [function], source_account: public_key_2)

source_account = Stellar.TxBuild.Account.new(public_key_2)
{:ok, seq_num} = Accounts.fetch_next_sequence_number(public_key)

args =
SCVec.new([
contract_address,
function_name,
address,
SCVal.new(u128: %{hi: 0, lo: 2})
])

host_function = HostFunction.new(invoke_contract: args)

invoke_host_function_op = InvokeHostFunction.new(host_function: host_function)

source_account = Stellar.TxBuild.Account.new(submitter_public_key)
{:ok, seq_num} = Accounts.fetch_next_sequence_number(submitter_public_key)
sequence_number = SequenceNumber.new(seq_num)
signature = Stellar.TxBuild.Signature.new(keypair2)
signature = Stellar.TxBuild.Signature.new(submitter_keypair)

# Use this XDR to simulate the transaction and get the soroban_data and min_resource_fee
# Use this XDR to simulate the transaction and get the soroban_data, the invoke_host_function auth and the min_resource_fee
source_account
|> Stellar.TxBuild.new(sequence_number: sequence_number)
|> Stellar.TxBuild.add_operation(invoke_host_function_op)
Expand All @@ -218,19 +207,27 @@ source_account
resources: %SorobanResources{instructions: %UInt32{datum: datum}} = resources
} = soroban_data,
""} =
"AAAAAwAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftYAAAAGg2ehMk/btW1B5qbOojZOOJ6fThfT6+qBDXveymY8LNUAAAAUAAAAB0YZgeyJouAsCYZs6da/3e+lEoTyazuT0hzldmfbT2xPAAAAAgAAAAaDZ6EyT9u1bUHmps6iNk44np9OF9Pr6oENe97KZjws1QAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAATAAAAAAAAAADJTnjlYcJxSKDat78jbEyDkVqo2uhpNX4BdBtqGrx+1gAAAAaDZ6EyT9u1bUHmps6iNk44np9OF9Pr6oENe97KZjws1QAAABUAAAAAAAAAAMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WABQR1AAAFLAAAAGoAAACZAAAAAAAAAB4AAAAAA=="
"AAAAAAAAAAIAAAAAAAAAAMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WAAAAB5g18rNSrgYpg/O7tgIlBv42+QqjpEFv6gEqW+oDFUZbAAAAAAAAAAIAAAAGAAAAAAAAAADJTnjlYcJxSKDat78jbEyDkVqo2uhpNX4BdBtqGrx+1gAAABU69WqNb/7SRQAAAAAAAAAAAAAABgAAAAEYaWENrVfrP3DtntX8suGy/f52r9ikRgXsGYWrbdStxwAAABQAAAABAAAAAAA4FMMAABWQAAABmAAAA/AAAAAAAAAAxQ=="
|> Base.decode64!()
|> SorobanTransactionData.decode_xdr!()

# Use the Soroban RPC `getLatestLedger` endpoint to obtain this number.
# This number needs to be in the same ledger when submitting the transaction, otherwise the function invocation will fail.
latest_ledger = 164_265

auth_xdr = "AAAAAQAAAAAAAAAAyU545WHCcUig2re/I2xMg5FaqNroaTV+AXQbahq8ftY69WqNb/7SRQAAAAAAAAAAAAAAAAAAAAEYaWENrVfrP3DtntX8suGy/f52r9ikRgXsGYWrbdStxwAAAANpbmMAAAAAAgAAABIAAAAAAAAAAMlOeOVhwnFIoNq3vyNsTIORWqja6Gk1fgF0G2oavH7WAAAACQAAAAAAAAAAAAAAAAAAAAIAAAAA"

auth = SorobanAuthorizationEntry.sign_xdr(auth_xdr, invoker_secret_key, latest_ledger)
invoke_host_function_op = InvokeHostFunction.set_auth(invoke_host_function_op, [auth])

# Needed calculations since simulate_transaction returns soroban_data
# with wrong calculated instructions value because there are two signers
new_instructions = UInt32.new(datum + round(datum * 0.25))
new_resources = %{resources | instructions: new_instructions}
soroban_data = %{soroban_data | resources: new_resources}

# Arbitrary additional fee(10_000)
min_resource_fee = 97_397 + 10_000
fee = BaseFee.new(min_resource_fee + 100)
# `round(min_resource_fee*0.1)` is needed since the cost of the transaction will increase because there are two signers.
fee = BaseFee.new(min_resource_fee + round(min_resource_fee*0.1) +100)

# Use the XDR generated here to send it to the futurenet
source_account
Expand Down
180 changes: 178 additions & 2 deletions lib/tx_build/soroban_authorization_entry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,36 @@ defmodule Stellar.TxBuild.SorobanAuthorizationEntry do
`SorobanAuthorizationEntry` struct definition.
"""

alias StellarBase.XDR.HashIDPreimage, as: HashIDPreimageXDR

alias StellarBase.XDR.HashIDPreimageSorobanAuthorization,
as: HashIDPreimageSorobanAuthorizationXDR

alias StellarBase.XDR.SCVec, as: SCVecXDR
alias StellarBase.XDR.SorobanAddressCredentials, as: SorobanAddressCredentialsXDR
alias StellarBase.XDR.SorobanAuthorizedInvocation, as: SorobanAuthorizedInvocationXDR
alias StellarBase.XDR.{EnvelopeType, Hash, Int64, SorobanAuthorizationEntry, UInt32}
alias Stellar.{KeyPair, Network}

alias Stellar.TxBuild.{
HashIDPreimage,
HashIDPreimageSorobanAuthorization,
SCMapEntry,
SCVal,
SCVec,
SorobanAddressCredentials,
SorobanCredentials,
SorobanAuthorizedInvocation
}

alias StellarBase.XDR.SorobanAuthorizationEntry

@behaviour Stellar.TxBuild.XDR

@type error :: {:error, atom()}
@type validation :: {:ok, any()} | error()
@type base_64 :: String.t()
@type secret_key :: String.t()
@type sign_authorization :: String.t()
@type latest_ledger :: non_neg_integer()
@type credentials :: SorobanCredentials.t()
@type root_invocation :: SorobanAuthorizedInvocation.t()

Expand Down Expand Up @@ -56,6 +75,119 @@ defmodule Stellar.TxBuild.SorobanAuthorizationEntry do

def to_xdr(_struct), do: {:error, :invalid_struct}

@spec sign(credentials :: t(), secret_key :: secret_key()) :: t() | error()
def sign(
%__MODULE__{
credentials: %SorobanCredentials{
value:
%SorobanAddressCredentials{
nonce: nonce,
signature_expiration_ledger: signature_expiration_ledger,
signature_args: signature_args
} = soroban_address_credentials
},
root_invocation: root_invocation
} = credentials,
secret_key
)
when is_binary(secret_key) do
{public_key, _secret_key} = KeyPair.from_secret_seed(secret_key)
raw_public_key = KeyPair.raw_public_key(public_key)
network_id = network_id_xdr()

signature =
[
network_id: network_id,
nonce: nonce,
signature_expiration_ledger: signature_expiration_ledger + 3,
invocation: root_invocation
]
|> HashIDPreimageSorobanAuthorization.new()
|> (&HashIDPreimage.new(soroban_auth: &1)).()
|> HashIDPreimage.to_xdr()
|> HashIDPreimageXDR.encode_xdr!()
|> hash()
|> KeyPair.sign(secret_key)

public_key_map_entry =
SCMapEntry.new(
SCVal.new(symbol: "public_key"),
SCVal.new(bytes: raw_public_key)
)

signature_map_entry =
SCMapEntry.new(
SCVal.new(symbol: "signature"),
SCVal.new(bytes: signature)
)

signature_sc_val = SCVal.new(map: [public_key_map_entry, signature_map_entry])

soroban_address_credentials = %{
soroban_address_credentials
| signature_args: SCVec.append_sc_val(signature_args, signature_sc_val)
}

%{credentials | credentials: soroban_address_credentials}
end

def sign(_args, _secret_key), do: {:error, :invalid_sign_args}

@spec sign_xdr(
base_64 :: base_64(),
secret_key :: secret_key(),
latest_ledger :: latest_ledger()
) :: sign_authorization() | error()
def sign_xdr(base_64, secret_key, latest_ledger)
when is_binary(base_64) and is_binary(secret_key) and is_integer(latest_ledger) do
{%SorobanAuthorizationEntry{
credentials:
%{
value:
%SorobanAddressCredentialsXDR{
nonce: nonce
} = soroban_address_credentials
} = credentials,
root_invocation: root_invocation
} = soroban_auth,
""} =
base_64
|> Base.decode64!()
|> SorobanAuthorizationEntry.decode_xdr!()

signature_expiration_ledger = UInt32.new(latest_ledger + 3)

signature_args =
nonce
|> build_signature_args_from_xdr(
signature_expiration_ledger,
root_invocation,
secret_key
)
|> SCVal.to_xdr()
|> (&SCVecXDR.new([&1])).()

soroban_address_credentials = %{
soroban_address_credentials
| signature_args: signature_args,
signature_expiration_ledger: signature_expiration_ledger
}

credentials = %{credentials | value: soroban_address_credentials}

%{soroban_auth | credentials: credentials}
|> SorobanAuthorizationEntry.encode_xdr!()
|> Base.encode64()
end

def sign_xdr(_base_64, _secret_key, _latest_ledger), do: {:error, :invalid_sign_args}

@spec network_id_xdr :: binary()
defp network_id_xdr, do: hash(Network.passphrase())

@spec hash(data :: binary()) :: binary()
defp hash(data), do: :crypto.hash(:sha256, data)

@spec validate_credentials(credentials :: credentials()) :: validation()
defp validate_credentials(%SorobanCredentials{} = credentials), do: {:ok, credentials}
defp validate_credentials(_credentials), do: {:error, :invalid_credentials}
Expand All @@ -65,4 +197,48 @@ defmodule Stellar.TxBuild.SorobanAuthorizationEntry do
do: {:ok, root_invocation}

defp validate_root_invocation(_root_invocation), do: {:error, :invalid_root_invocation}

@spec build_signature_args_from_xdr(
nonce :: Int64.t(),
signature_expiration_ledger :: UInt32.t(),
root_invocation :: SorobanAuthorizedInvocationXDR.t(),
secret_key :: secret_key()
) :: SCVal.t() | error()
defp build_signature_args_from_xdr(
nonce,
signature_expiration_ledger,
root_invocation,
secret_key
) do
{public_key, _secret_key} = KeyPair.from_secret_seed(secret_key)
raw_public_key = KeyPair.raw_public_key(public_key)
envelope_type = EnvelopeType.new(:ENVELOPE_TYPE_SOROBAN_AUTHORIZATION)

signature =
network_id_xdr()
|> Hash.new()
|> HashIDPreimageSorobanAuthorizationXDR.new(
nonce,
signature_expiration_ledger,
root_invocation
)
|> HashIDPreimageXDR.new(envelope_type)
|> HashIDPreimageXDR.encode_xdr!()
|> hash()
|> KeyPair.sign(secret_key)

public_key_map_entry =
SCMapEntry.new(
SCVal.new(symbol: "public_key"),
SCVal.new(bytes: raw_public_key)
)

signature_map_entry =
SCMapEntry.new(
SCVal.new(symbol: "signature"),
SCVal.new(bytes: signature)
)

SCVal.new(map: [public_key_map_entry, signature_map_entry])
end
end
Loading

0 comments on commit b5a436a

Please sign in to comment.