Skip to content

Commit 9780294

Browse files
authored
Merge pull request #21 from wowica/local-tx-submission
Local tx submission
2 parents 039ff56 + cc6d597 commit 9780294

File tree

13 files changed

+675
-49
lines changed

13 files changed

+675
-49
lines changed

Diff for: lib/transaction/response.ex

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
defmodule Xander.Transaction.Response do
2+
@moduledoc """
3+
Parses the response from a transaction. Accepts CBOR encoded responses.
4+
"""
5+
6+
require Logger
7+
8+
@doc """
9+
Parses the response from a transaction. Accepts CBOR encoded responses.
10+
11+
## Examples
12+
13+
# Transaction accepted
14+
iex> binary = Xander.Util.plex_encode(CBOR.encode([1]))
15+
iex> Xander.Transaction.Response.parse_response(binary)
16+
{:ok, :accepted}
17+
18+
# Transaction rejected with reason
19+
iex> tag = %CBOR.Tag{tag: :bytes, value: "invalid tx"}
20+
iex> binary = Xander.Util.plex_encode(CBOR.encode([2, tag]))
21+
iex> Xander.Transaction.Response.parse_response(binary)
22+
{:rejected, %CBOR.Tag{tag: :bytes, value: "invalid tx"}}
23+
24+
# Disconnected from node
25+
iex> binary = Xander.Util.plex_encode(CBOR.encode([3]))
26+
iex> Xander.Transaction.Response.parse_response(binary)
27+
{:error, :disconnected}
28+
29+
# Error handling invalid CBOR
30+
iex> Xander.Transaction.Response.parse_response(<<0, 1, 2, 3>>)
31+
{:error, :invalid_format}
32+
33+
# Error handling nil input
34+
iex> Xander.Transaction.Response.parse_response(nil)
35+
{:error, :invalid_input}
36+
"""
37+
@spec parse_response(binary() | nil) ::
38+
{:error,
39+
:cannot_decode_non_binary_values
40+
| :cbor_function_clause_error
41+
| :cbor_match_error
42+
| :invalid_format
43+
| :invalid_input
44+
| :disconnected}
45+
| {:rejected, any()}
46+
| {:ok, :accepted}
47+
def parse_response(cbor_response) do
48+
case Xander.Util.plex(cbor_response) do
49+
{:ok, %{payload: cbor_response_payload}} ->
50+
decode(cbor_response_payload)
51+
52+
{:error, reason} ->
53+
{:error, reason}
54+
end
55+
end
56+
57+
defp decode(cbor_response_payload) do
58+
case CBOR.decode(cbor_response_payload) do
59+
{:ok, [1], <<>>} ->
60+
{:ok, :accepted}
61+
62+
{:ok, [2, failure_reason], <<>>} ->
63+
{:rejected, failure_reason}
64+
65+
{:ok, [3], <<>>} ->
66+
{:error, :disconnected}
67+
68+
{:error, error} ->
69+
{:error, error}
70+
end
71+
end
72+
end

Diff for: lib/xander/config.ex

+3-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ defmodule Xander.Config do
6060

6161
# Validates and normalizes the configuration options.
6262
defp validate_config!(opts) do
63-
if opts[:network] not in [:mainnet, :preprod, :preview, :sanchonet] do
64-
raise ArgumentError, "network must be :mainnet, :preprod, :preview, or :sanchonet"
63+
if opts[:network] not in [:mainnet, :preprod, :preview, :sanchonet, :yaci_devkit] do
64+
raise ArgumentError,
65+
"network must be :mainnet, :preprod, :preview, :sanchonet, or :yaci_devkit"
6566
end
6667

6768
if opts[:type] not in [:socket, :ssl] do

Diff for: lib/xander/handshake/proposal.ex

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ defmodule Xander.Handshake.Proposal do
33
Builds handshake messages for node-to-client communication.
44
"""
55

6-
@type network_type :: :mainnet | :preprod | :preview | :sanchonet
6+
@type network_type :: :mainnet | :preprod | :preview | :sanchonet | :yaci_devkit
77

88
@network_magic [
99
mainnet: 764_824_073,
1010
preprod: 1,
1111
preview: 2,
12-
sanchonet: 4
12+
sanchonet: 4,
13+
yaci_devkit: 42
1314
]
1415

1516
@version_numbers %{

Diff for: lib/xander/handshake/response.ex

+121-36
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,133 @@ defmodule Xander.Handshake.Response do
88

99
alias Xander.Util
1010

11+
@supported_versions [32783, 32784, 32785]
12+
@msg_accept_version 1
13+
@msg_refuse 2
14+
@msg_query_reply 3
15+
1116
@doc """
1217
Validates the handshake response from a Cardano node.
18+
19+
## Examples
20+
21+
# Valid msgAcceptVersion response
22+
iex> payload = CBOR.encode([1, 32784, [764824073, %{"versions" => [1, 2, 3]}]])
23+
iex> response = Xander.Util.plex_encode(payload)
24+
iex> {:ok, result} = Xander.Handshake.Response.validate(response)
25+
iex> result.type == :msg_accept_version and result.version_number == 32784
26+
true
27+
28+
# Unsupported version in msgAcceptVersion
29+
iex> payload = CBOR.encode([1, 12345, [764824073, %{"versions" => [1, 2, 3]}]])
30+
iex> response = Xander.Util.plex_encode(payload)
31+
iex> Xander.Handshake.Response.validate(response)
32+
{:error, "Only versions [32783, 32784, 32785] are supported."}
33+
34+
# Version mismatch refuse response
35+
iex> payload = CBOR.encode([2, [0, <<1, 2, 3, 4>>]])
36+
iex> response = Xander.Util.plex_encode(payload)
37+
iex> {:refused, result} = Xander.Handshake.Response.validate(response)
38+
iex> result.type
39+
:version_mismatch
40+
41+
# Handshake decode error refuse response
42+
iex> payload = CBOR.encode([2, [1, 32784, "decode error"]])
43+
iex> response = Xander.Util.plex_encode(payload)
44+
iex> {:refused, result} = Xander.Handshake.Response.validate(response)
45+
iex> result.type
46+
:handshake_decode_error
47+
48+
# Refused version refuse response
49+
iex> payload = CBOR.encode([2, [2, 32784, "refused"]])
50+
iex> response = Xander.Util.plex_encode(payload)
51+
iex> {:refused, result} = Xander.Handshake.Response.validate(response)
52+
iex> result.type
53+
:refused
54+
55+
# Unknown refuse reason
56+
iex> payload = CBOR.encode([2, [99, "unknown"]])
57+
iex> response = Xander.Util.plex_encode(payload)
58+
iex> {:refused, result} = Xander.Handshake.Response.validate(response)
59+
iex> result.type
60+
:unknown_refuse_reason
61+
62+
# Query reply response
63+
iex> tag = %CBOR.Tag{tag: :bytes, value: "supported"}
64+
iex> payload = CBOR.encode([3, %{tag => [32783, 32784, 32785]}])
65+
iex> response = Xander.Util.plex_encode(payload)
66+
iex> Xander.Handshake.Response.validate(response)
67+
{:versions, %{%CBOR.Tag{tag: :bytes, value: "supported"} => [32783, 32784, 32785]}}
68+
69+
# Unknown message type
70+
iex> payload = CBOR.encode([99, "unknown"])
71+
iex> response = Xander.Util.plex_encode(payload)
72+
iex> Xander.Handshake.Response.validate(response)
73+
{:error, "Unknown message format"}
74+
75+
# Error in CBOR decoding
76+
iex> response = <<0, 1, 2, 3>> # Invalid CBOR
77+
iex> Xander.Handshake.Response.validate(response)
78+
{:error, :invalid_format}
1379
"""
1480
def validate(response) do
15-
%{payload: payload} = Util.plex(response)
81+
with {:ok, %{payload: payload}} <- Util.plex(response),
82+
{:ok, decoded} <- decode_cbor(payload) do
83+
process_decoded_message(decoded)
84+
end
85+
end
1686

87+
defp decode_cbor(payload) do
1788
case CBOR.decode(payload) do
18-
# msgAcceptVersion
19-
{:ok, [1, version, [magic, query]], ""} ->
20-
if version in [32783, 32784] do
21-
{:ok,
22-
%__MODULE__{
23-
network_magic: magic,
24-
query: query,
25-
type: :msg_accept_version,
26-
version_number: version
27-
}}
28-
else
29-
{:error, "Only versions 32783 and 32784 are supported."}
30-
end
31-
32-
# msgRefuse
33-
{:ok, [2, refuse_reason], ""} ->
34-
case refuse_reason do
35-
# TODO: return accepted versions; reduce to 32783 and 32784
36-
[0, _version_number_binary] ->
37-
{:refused, %__MODULE__{type: :version_mismatch}}
38-
39-
[1, _anyVersionNumber, _tstr] ->
40-
{:refused, %__MODULE__{type: :handshake_decode_error}}
41-
42-
[2, _anyVersionNumber, _tstr] ->
43-
{:refused, %__MODULE__{type: :refused}}
44-
end
45-
46-
# TODO: parse version_table
47-
# msgQueryReply
48-
{:ok, [3, version_table], ""} ->
49-
{:versions, version_table}
50-
51-
{:error, reason} ->
52-
{:error, reason}
89+
{:ok, decoded, ""} -> {:ok, decoded}
90+
{:error, reason} -> {:error, reason}
91+
end
92+
end
93+
94+
# Handle msgAcceptVersion (1)
95+
defp process_decoded_message([@msg_accept_version, version, [magic, query]]) do
96+
if version in @supported_versions do
97+
{:ok,
98+
%__MODULE__{
99+
network_magic: magic,
100+
query: query,
101+
type: :msg_accept_version,
102+
version_number: version
103+
}}
104+
else
105+
{:error, "Only versions #{inspect(@supported_versions)} are supported."}
53106
end
54107
end
108+
109+
# Handle msgRefuse (2)
110+
defp process_decoded_message([@msg_refuse, refuse_reason]) do
111+
process_refuse_reason(refuse_reason)
112+
end
113+
114+
# Handle msgQueryReply (3)
115+
defp process_decoded_message([@msg_query_reply, version_table]) do
116+
# TODO: parse version_table
117+
{:versions, version_table}
118+
end
119+
120+
defp process_decoded_message(_) do
121+
{:error, "Unknown message format"}
122+
end
123+
124+
defp process_refuse_reason([0, _version_number_binary]) do
125+
# TODO: return accepted versions; reduce to 32783-32785
126+
{:refused, %__MODULE__{type: :version_mismatch}}
127+
end
128+
129+
defp process_refuse_reason([1, _any_version_number, _tstr]) do
130+
{:refused, %__MODULE__{type: :handshake_decode_error}}
131+
end
132+
133+
defp process_refuse_reason([2, _any_version_number, _tstr]) do
134+
{:refused, %__MODULE__{type: :refused}}
135+
end
136+
137+
defp process_refuse_reason(_) do
138+
{:refused, %__MODULE__{type: :unknown_refuse_reason}}
139+
end
55140
end

Diff for: lib/xander/messages.ex

+25
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ defmodule Xander.Messages do
2222
@message_acquire [8]
2323
@message_release [5]
2424

25+
# See the CDDL for details on mapping of messages to numbers.
26+
# https://github.com/IntersectMBO/ouroboros-network/blob/main/ouroboros-network-protocols/cddl/specs/local-tx-submission.cddl
27+
@message_submit_tx 0
28+
@conway_era 6
29+
# Tag number 24 (CBOR data item) can be used to tag the embedded
30+
# byte string as a single data item encoded in CBOR format.
31+
# https://datatracker.ietf.org/doc/html/rfc8949#embedded-di
32+
@encoded_cbor_tag 24
33+
2534
@doc """
2635
Acquires a snapshot of the mempool, allowing the protocol to make queries.
2736
@@ -133,8 +142,24 @@ defmodule Xander.Messages do
133142
header(@mini_protocols.local_state_query, bitstring_payload) <> bitstring_payload
134143
end
135144

145+
@spec transaction(binary()) :: binary()
146+
def transaction(tx_hex) do
147+
bitstring_payload =
148+
tx_hex
149+
|> Base.decode16!(case: :mixed)
150+
|> build_transaction()
151+
|> CBOR.encode()
152+
153+
header(@mini_protocols.local_tx_submission, bitstring_payload) <> bitstring_payload
154+
end
155+
136156
defp build_query(query), do: [@message_query, query]
137157

158+
defp build_transaction(tx_binary) do
159+
tag = %CBOR.Tag{tag: :bytes, value: tx_binary}
160+
[@message_submit_tx, [@conway_era, %CBOR.Tag{tag: @encoded_cbor_tag, value: tag}]]
161+
end
162+
138163
# middle 16 bits are: 1 bit == 0 for initiator and 15 bits for the mini protocol ID
139164
defp header(mini_protocol_id, payload),
140165
do:

Diff for: lib/xander/query.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ defmodule Xander.Query do
1313
require Logger
1414

1515
@basic_tcp_opts [:binary, active: false, send_timeout: 4_000]
16-
@active_n2c_versions [9, 10, 11, 12, 13, 14, 15, 16]
16+
@active_n2c_versions [9, 10, 11, 12, 13, 14, 15, 16, 17]
1717

1818
defstruct [:client, :path, :port, :socket, :network, queue: :queue.new()]
1919

Diff for: lib/xander/query/response.ex

+50-4
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,60 @@ defmodule Xander.Query.Response do
88

99
@doc """
1010
Parses the response from a query. Accepts CBOR encoded responses.
11+
12+
## Examples
13+
14+
# Current tip query response (slot number and block hash)
15+
iex> block_bytes = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15>>
16+
iex> payload = CBOR.encode([4, [12345, %CBOR.Tag{tag: :bytes, value: block_bytes}]])
17+
iex> binary = Xander.Util.plex_encode(payload)
18+
iex> Xander.Query.Response.parse_response(binary)
19+
{:ok, {12345, "000102030405060708090a0b0c0d0e0f"}}
20+
21+
# Current block height query response
22+
iex> payload = CBOR.encode([4, [1, 123456]])
23+
iex> binary = Xander.Util.plex_encode(payload)
24+
iex> Xander.Query.Response.parse_response(binary)
25+
{:ok, 123456}
26+
27+
# Epoch number query response
28+
iex> payload = CBOR.encode([4, [42]])
29+
iex> binary = Xander.Util.plex_encode(payload)
30+
iex> Xander.Query.Response.parse_response(binary)
31+
{:ok, 42}
32+
33+
# Generic response
34+
iex> tag = %CBOR.Tag{tag: :bytes, value: "some data"}
35+
iex> payload = CBOR.encode([4, tag])
36+
iex> binary = Xander.Util.plex_encode(payload)
37+
iex> Xander.Query.Response.parse_response(binary)
38+
{:ok, %CBOR.Tag{tag: :bytes, value: "some data"}}
39+
40+
# Error case - invalid CBOR format
41+
iex> payload = CBOR.encode(["not a valid response"])
42+
iex> binary = Xander.Util.plex_encode(payload)
43+
iex> Xander.Query.Response.parse_response(binary)
44+
{:error, :invalid_cbor}
45+
46+
# Error case - invalid binary format
47+
iex> Xander.Query.Response.parse_response(<<0, 1, 2, 3>>)
48+
{:error, :invalid_format}
49+
50+
# Error case - nil input
51+
iex> Xander.Query.Response.parse_response(nil)
52+
{:error, :invalid_input}
1153
"""
1254
@spec parse_response(cbor()) :: {:ok, any()} | {:error, atom()}
1355
def parse_response(cbor_response) do
14-
%{payload: cbor_response_payload} = Xander.Util.plex(cbor_response)
56+
case Xander.Util.plex(cbor_response) do
57+
{:ok, %{payload: cbor_response_payload}} ->
58+
case CBOR.decode(cbor_response_payload) do
59+
{:ok, decoded, ""} -> parse_cbor(decoded)
60+
{:error, _reason} -> {:error, :error_decoding_cbor}
61+
end
1562

16-
case CBOR.decode(cbor_response_payload) do
17-
{:ok, decoded, ""} -> parse_cbor(decoded)
18-
{:error, _reason} -> {:error, :error_decoding_cbor}
63+
{:error, reason} ->
64+
{:error, reason}
1965
end
2066
end
2167

0 commit comments

Comments
 (0)