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 3 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
75 changes: 32 additions & 43 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 Down Expand Up @@ -133,19 +134,15 @@ 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

```elixir
alias StellarBase.XDR.{SorobanResources, SorobanTransactionData, UInt32}

alias Stellar.TxBuild.{
ContractAuth,
AddressWithNonce,
AuthorizedInvocation,
BaseFee,
InvokeHostFunction,
HostFunction,
HostFunctionArgs,
SCVal,
SCAddress,
SequenceNumber,
Expand All @@ -155,8 +152,11 @@ 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} =
Expand All @@ -169,45 +169,25 @@ keypair2 =

address_type = SCAddress.new(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)

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(public_key_2)
{:ok, seq_num} = Accounts.fetch_next_sequence_number(public_key)
{:ok, seq_num} = Accounts.fetch_next_sequence_number(public_key_2)
Odraxs marked this conversation as resolved.
Show resolved Hide resolved
sequence_number = SequenceNumber.new(seq_num)
signature = Stellar.TxBuild.Signature.new(keypair2)

# 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,18 +198,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 sending the transaction envelope to the `sendTransaction` endpoint, otherwise the function invocation would fail.
Odraxs marked this conversation as resolved.
Show resolved Hide resolved
latest_ledger = 164_265

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

auth = SorobanAuthorizationEntry.sign_xdr(auth_xdr, 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
min_resource_fee = 115_070 + 10_000
Odraxs marked this conversation as resolved.
Show resolved Hide resolved
fee = BaseFee.new(min_resource_fee + 100)

# Use the XDR generated here to send it to the futurenet
Expand Down
181 changes: 179 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,49 @@ 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)
network_id = network_id_xdr()
envelope_type = EnvelopeType.new(:ENVELOPE_TYPE_SOROBAN_AUTHORIZATION)

signature =
network_id
|> Hash.new()
|> HashIDPreimageSorobanAuthorizationXDR.new(
nonce,
signature_expiration_ledger,
root_invocation
)
|> HashIDPreimageXDR.new(envelope_type)
|> HashIDPreimageXDR.encode_xdr!()
|> hash()
|> KeyPair.sign(secret_key)
Odraxs marked this conversation as resolved.
Show resolved Hide resolved

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
25 changes: 0 additions & 25 deletions lib/tx_build/variable_opaque.ex

This file was deleted.

19 changes: 19 additions & 0 deletions test/support/xdr_fixtures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Stellar.Test.XDRFixtures do
alias Stellar.TxBuild.Asset, as: TxAsset
alias Stellar.TxBuild.AccountID, as: TxAccountID
alias Stellar.TxBuild.SequenceNumber, as: TxSequenceNumber
alias Stellar.TxBuild.SorobanAuthorizationEntry, as: TxSorobanAuthorizationEntry

alias StellarBase.XDR.{
AccountID,
Expand Down Expand Up @@ -809,6 +810,24 @@ defmodule Stellar.Test.XDRFixtures do
|> OperationBody.new(op_type)
end

@spec invoke_host_function_op_xdr(
function :: TxHostFunction.t(),
auths :: list(TxSorobanAuthorizationEntry.t())
) :: OperationBody.t()
def invoke_host_function_op_xdr(function, auths) do
op_type = OperationType.new(:INVOKE_HOST_FUNCTION)

auths =
auths
|> Enum.map(&TxSorobanAuthorizationEntry.to_xdr/1)
|> SorobanAuthorizationEntryList.new()

function
|> TxHostFunction.to_xdr()
|> InvokeHostFunction.new(auths)
|> OperationBody.new(op_type)
end

@spec build_asset_xdr(asset :: any()) :: list(Asset.t())
defp build_asset_xdr(:native), do: create_asset_native_xdr()

Expand Down
Loading