Skip to content

Commit f1941d4

Browse files
authored
feat: Offline multi node signing support (#1378)
* feat: add signable body bytes and addSignature for multi-chunk/multi-node transactions Signed-off-by: Ivan Ivanov <[email protected]> * fix: add check if already signed Signed-off-by: Ivan Ivanov <[email protected]> * chore: rename apis Signed-off-by: Ivan Ivanov <[email protected]> * test: unit AddSignatureForMultiNodeMultiChunk Signed-off-by: Ivan Ivanov <[email protected]> * test: GetSignableNodeBodyBytesList unit Signed-off-by: Ivan Ivanov <[email protected]> * chore: add example Signed-off-by: Ivan Ivanov <[email protected]> * chore: refactor Signed-off-by: Ivan Ivanov <[email protected]> * chore: rename func Signed-off-by: Ivan Ivanov <[email protected]> * chore: address PR feedback Signed-off-by: Ivan Ivanov <[email protected]> * chore: remove reflection Signed-off-by: Ivan Ivanov <[email protected]> --------- Signed-off-by: Ivan Ivanov <[email protected]>
1 parent 71fc0b8 commit f1941d4

File tree

7 files changed

+864
-1
lines changed

7 files changed

+864
-1
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
hiero "github.com/hiero-ledger/hiero-sdk-go/v2/sdk"
9+
)
10+
11+
func main() {
12+
var client *hiero.Client
13+
var err error
14+
15+
// Retrieving network type from environment variable HEDERA_NETWORK
16+
client, err = hiero.ClientForName(os.Getenv("HEDERA_NETWORK"))
17+
if err != nil {
18+
panic(fmt.Sprintf("%v : error creating client", err))
19+
}
20+
21+
// Retrieving operator ID from environment variable OPERATOR_ID
22+
operatorAccountID, err := hiero.AccountIDFromString(os.Getenv("OPERATOR_ID"))
23+
if err != nil {
24+
panic(fmt.Sprintf("%v : error converting string to AccountID", err))
25+
}
26+
27+
// Retrieving operator key from environment variable OPERATOR_KEY
28+
operatorKey, err := hiero.PrivateKeyFromString(os.Getenv("OPERATOR_KEY"))
29+
if err != nil {
30+
panic(fmt.Sprintf("%v : error converting string to PrivateKey", err))
31+
}
32+
33+
// Setting the client operator ID and key
34+
client.SetOperator(operatorAccountID, operatorKey)
35+
36+
defer client.Close()
37+
38+
// Generate keys for sender and receiver
39+
senderKey, err := hiero.GeneratePrivateKey()
40+
if err != nil {
41+
panic(fmt.Sprintf("Failed to generate sender private key: %v", err))
42+
}
43+
receiverKey, err := hiero.GeneratePrivateKey()
44+
if err != nil {
45+
panic(fmt.Sprintf("Failed to generate receiver private key: %v", err))
46+
}
47+
48+
// Create accounts
49+
senderAccountResponse, err := hiero.NewAccountCreateTransaction().
50+
SetKeyWithoutAlias(senderKey.PublicKey()).
51+
SetInitialBalance(hiero.NewHbar(10)).
52+
Execute(client)
53+
if err != nil {
54+
panic(fmt.Sprintf("Failed to execute sender account creation transaction: %v", err))
55+
}
56+
57+
senderAccountReceipt, err := senderAccountResponse.GetReceipt(client)
58+
if err != nil {
59+
panic(fmt.Sprintf("Failed to get receipt for sender account creation: %v", err))
60+
}
61+
senderId := *senderAccountReceipt.AccountID
62+
63+
receiverAccountResponse, err := hiero.NewAccountCreateTransaction().
64+
SetKeyWithoutAlias(receiverKey).
65+
SetInitialBalance(hiero.NewHbar(1)).
66+
Execute(client)
67+
if err != nil {
68+
panic(fmt.Sprintf("Failed to execute receiver account creation transaction: %v", err))
69+
}
70+
71+
receiverAccountReceipt, err := receiverAccountResponse.GetReceipt(client)
72+
if err != nil {
73+
panic(fmt.Sprintf("Failed to get receipt for receiver account creation: %v", err))
74+
}
75+
receiverId := *receiverAccountReceipt.AccountID
76+
77+
// Single node transaction example
78+
if err := singleNodeTransactionExample(client, senderId, receiverId, senderKey); err != nil {
79+
panic(fmt.Sprintf("Single node transaction example failed: %v", err))
80+
}
81+
82+
// Multi-node multi-chunk transaction example
83+
if err := multiNodeFileTransactionExample(client, senderId, senderKey); err != nil {
84+
panic(fmt.Sprintf("Multi-node file transaction example failed: %v", err))
85+
}
86+
}
87+
88+
func singleNodeTransactionExample(client *hiero.Client, senderId, receiverId hiero.AccountID, senderKey hiero.PrivateKey) error {
89+
// Step 1 - Create and prepare transfer transaction
90+
// Get first node from network
91+
network := client.GetNetwork()
92+
var nodeAccountId hiero.AccountID
93+
for _, node := range network {
94+
nodeAccountId = node
95+
break
96+
}
97+
98+
// Create transfer transaction
99+
transferTx := hiero.NewTransferTransaction().
100+
AddHbarTransfer(senderId, hiero.NewHbar(-1)).
101+
AddHbarTransfer(receiverId, hiero.NewHbar(1)).
102+
SetNodeAccountIDs([]hiero.AccountID{nodeAccountId}).
103+
SetTransactionID(hiero.TransactionIDGenerate(senderId))
104+
105+
transferTx, err := transferTx.FreezeWith(client)
106+
if err != nil {
107+
return fmt.Errorf("failed to freeze transfer transaction: %w", err)
108+
}
109+
110+
// Step 2 - Get signable bytes and sign with HSM
111+
signableList, err := transferTx.GetSignableNodeBodyBytesList()
112+
if err != nil {
113+
return fmt.Errorf("failed to get signable bytes list: %w", err)
114+
}
115+
116+
// Sign with HSM for each entry
117+
for _, signable := range signableList {
118+
signature := hsmSign(senderKey, signable.Body)
119+
transferTx, err = transferTx.AddSignatureV2(senderKey.PublicKey(), signature, signable.TransactionID, signable.NodeID)
120+
if err != nil {
121+
return fmt.Errorf("failed to add signature: %w", err)
122+
}
123+
}
124+
125+
// Step 3 - Execute transaction and get receipt
126+
transferResponse, err := transferTx.Execute(client)
127+
if err != nil {
128+
return fmt.Errorf("failed to execute transfer transaction: %w", err)
129+
}
130+
131+
transferReceipt, err := transferResponse.GetReceipt(client)
132+
if err != nil {
133+
return fmt.Errorf("failed to get transfer receipt: %w", err)
134+
}
135+
136+
fmt.Printf("Single node transaction status: %v\n", transferReceipt.Status)
137+
return nil
138+
}
139+
140+
func multiNodeFileTransactionExample(client *hiero.Client, senderId hiero.AccountID, senderKey hiero.PrivateKey) error {
141+
// Step 1 - Create initial file
142+
// Create large content for testing
143+
bigContents := strings.Repeat("Lorem ipsum dolor sit amet. ", 1000)
144+
145+
// Create file transaction
146+
fileCreateTx := hiero.NewFileCreateTransaction().
147+
SetKeys(senderKey.PublicKey()).
148+
SetContents([]byte("[e2e::FileCreateTransaction]")).
149+
SetMaxTransactionFee(hiero.NewHbar(5))
150+
151+
fileCreateTx, err := fileCreateTx.FreezeWith(client)
152+
if err != nil {
153+
return fmt.Errorf("failed to freeze file create transaction: %w", err)
154+
}
155+
156+
fileCreateResponse, err := fileCreateTx.Sign(senderKey).Execute(client)
157+
if err != nil {
158+
return fmt.Errorf("failed to execute file create transaction: %w", err)
159+
}
160+
161+
fileCreateReceipt, err := fileCreateResponse.GetReceipt(client)
162+
if err != nil {
163+
return fmt.Errorf("failed to get file create receipt: %w", err)
164+
}
165+
166+
fileId := *fileCreateReceipt.FileID
167+
fmt.Printf("Created file with ID: %v\n", fileId)
168+
169+
// Step 2 - Prepare file append transaction
170+
fileAppendTx := hiero.NewFileAppendTransaction().
171+
SetFileID(fileId).
172+
SetContents([]byte(bigContents)).
173+
SetMaxTransactionFee(hiero.NewHbar(5)).
174+
SetTransactionID(hiero.TransactionIDGenerate(senderId))
175+
176+
fileAppendTx, err = fileAppendTx.FreezeWith(client)
177+
if err != nil {
178+
return fmt.Errorf("failed to freeze file append transaction: %w", err)
179+
}
180+
181+
// Step 3 - Get signable bytes and sign with HSM for each node
182+
fmt.Printf("Signing transaction with HSM for nodes: %v\n", fileAppendTx.GetNodeAccountIDs())
183+
184+
multiNodeSignableList, err := fileAppendTx.GetSignableNodeBodyBytesList()
185+
if err != nil {
186+
return fmt.Errorf("failed to get signable bytes list: %w", err)
187+
}
188+
189+
// Sign with HSM for each entry
190+
for _, signable := range multiNodeSignableList {
191+
signature := hsmSign(senderKey, signable.Body)
192+
fileAppendTx, err = fileAppendTx.AddSignatureV2(senderKey.PublicKey(), signature, signable.TransactionID, signable.NodeID)
193+
if err != nil {
194+
return fmt.Errorf("failed to add signature: %w", err)
195+
}
196+
}
197+
198+
// Step 4 - Execute transaction and verify results
199+
fileAppendResponse, err := fileAppendTx.Execute(client)
200+
if err != nil {
201+
return fmt.Errorf("failed to execute file append transaction: %w", err)
202+
}
203+
204+
fileAppendReceipt, err := fileAppendResponse.GetReceipt(client)
205+
if err != nil {
206+
return fmt.Errorf("failed to get file append receipt: %w", err)
207+
}
208+
209+
fmt.Printf("Multi-node file append transaction status: %v\n", fileAppendReceipt.Status)
210+
211+
// Step 5 - Verify file contents
212+
contents, err := hiero.NewFileContentsQuery().
213+
SetFileID(fileId).
214+
Execute(client)
215+
if err != nil {
216+
return fmt.Errorf("failed to query file contents: %w", err)
217+
}
218+
219+
fmt.Printf("File content length according to FileContentsQuery: %d\n", len(contents))
220+
return nil
221+
}
222+
223+
// hsmSign simulates signing with an HSM.
224+
// In a real implementation, this would use actual HSM SDK logic.
225+
func hsmSign(key hiero.PrivateKey, bodyBytes []byte) []byte {
226+
// This is a placeholder that simulates HSM signing
227+
return key.Sign(bodyBytes)
228+
}

sdk/crypto.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,7 @@ func (pk PublicKey) _ToSignaturePairProtobuf(signature []byte) *services.Signatu
869869
// SignTransaction signes the transaction and adds the signature to the transaction
870870
func (sk PrivateKey) SignTransaction(tx TransactionInterface) ([]byte, error) {
871871
baseTx := tx.getBaseTransaction()
872+
// TODO: call tx.addSignature
872873

873874
if sk.ecdsaPrivateKey != nil {
874875
b, err := sk.ecdsaPrivateKey._SignTransaction(baseTx)

sdk/file_append_transaction_e2e_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,62 @@ func TestIntegrationFileAppendTransactionCanExecute(t *testing.T) {
6262

6363
}
6464

65+
func TestIntegrationFileAppendTransactionSignForMultipleNodes(t *testing.T) {
66+
t.Parallel()
67+
env := NewIntegrationTestEnv(t)
68+
defer CloseIntegrationTestEnv(env, nil)
69+
70+
newKey, err := PrivateKeyGenerateEd25519()
71+
require.NoError(t, err)
72+
73+
tx, err := NewFileCreateTransaction().
74+
SetKeys(newKey.PublicKey()).
75+
SetNodeAccountIDs(env.NodeAccountIDs).
76+
SetContents([]byte("Hello")).
77+
SetTransactionMemo("go sdk e2e tests").
78+
FreezeWith(env.Client)
79+
require.NoError(t, err)
80+
81+
tx.Sign(newKey)
82+
83+
resp, err := tx.Execute(env.Client)
84+
require.NoError(t, err)
85+
86+
receipt, err := resp.SetValidateStatus(true).GetReceipt(env.Client)
87+
require.NoError(t, err)
88+
89+
fileID := *receipt.FileID
90+
assert.NotNil(t, fileID)
91+
92+
tx1, err := NewFileAppendTransaction().
93+
SetFileID(fileID).
94+
SetContents([]byte(LARGE_SMART_CONTRACT_BYTECODE)).
95+
FreezeWith(env.Client)
96+
require.NoError(t, err)
97+
98+
signableBodyList, err := tx1.GetSignableNodeBodyBytesList()
99+
require.NoError(t, err)
100+
for _, signableBody := range signableBodyList {
101+
signature := newKey.Sign(signableBody.Body)
102+
tx1, err = tx1.AddSignatureV2(newKey.PublicKey(), signature, signableBody.TransactionID, signableBody.NodeID)
103+
require.NoError(t, err)
104+
}
105+
106+
resp, err = tx1.Execute(env.Client)
107+
require.NoError(t, err)
108+
109+
_, err = resp.SetValidateStatus(true).GetReceipt(env.Client)
110+
require.NoError(t, err)
111+
112+
contents, err := NewFileContentsQuery().
113+
SetFileID(fileID).
114+
SetNodeAccountIDs([]AccountID{resp.NodeID}).
115+
Execute(env.Client)
116+
require.NoError(t, err)
117+
118+
assert.Equal(t, len(LARGE_SMART_CONTRACT_BYTECODE+"Hello"), len(contents))
119+
}
120+
65121
func TestIntegrationFileAppendTransactionNoFileID(t *testing.T) {
66122
t.Parallel()
67123
env := NewIntegrationTestEnv(t)

0 commit comments

Comments
 (0)