Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support authorized invocation with different accounts #321

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Odraxs marked this conversation as resolved.
Show resolved Hide resolved

> **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